With replacements, Sequelize escapes your values and inserts them into the SQL string before sending it. With bind parameters, the values are sent to the database separately from the query text. Both avoid hand concatenation. Bind parameters keep values out of the SQL entirely, which is the stronger property.
What the documentation says
The Sequelize raw queries guide describes replacements as used in two ways, with named parameters starting with a colon or unnamed ones written as a question mark. It says the replacements are escaped and inserted into the query by Sequelize before the query is sent to the database. It then describes bind parameters as sent to the database outside the SQL query text.
The guide adds a rule you should remember: a query can have either bind parameters or replacements, not both.
| Feature | Replacements | Bind parameters |
|---|---|---|
| Syntax | Colon name, or question mark | Dollar number, or dollar name |
| Where the value goes | Escaped and inserted into the SQL by Sequelize | Sent to the database outside the SQL text |
| Mixed in one query | No | No |
| Who protects against bad input | The library's escaping | The database's parameter handling |
Why the difference matters
With replacements, the final SQL string contains your value. Protection depends on the library's escaping being correct for your dialect and settings. With bind parameters, the SQL text is fixed before any value arrives, so a value cannot change the structure of the statement.
Escaping is well tested in mature libraries, so replacements are not unsafe by design. But a long history of injection bugs comes from escaping assumptions that failed in a corner case. A design that never puts the value in the SQL text removes that class of problem.
Examples
// Replacements: the library builds the final SQL
await sequelize.query(
'SELECT * FROM projects WHERE status = :status',
{ replacements: { status: 'active' }, type: QueryTypes.SELECT }
);
// Bind parameters: values are sent separately
await sequelize.query(
'SELECT * FROM projects WHERE status = $status',
{ bind: { status: 'active' }, type: QueryTypes.SELECT }
);
The documentation says an exception is thrown if a replacement object has keys not found in the query, or the reverse, which helps catch typos.
What neither one fixes
Neither mechanism protects an identifier or a keyword. A column name in ORDER BY, or a table name, cannot be passed as a value. Use an allowlist in code for those.
const sortable = { created: 'created_at', name: 'name' };
const col = sortable[req.query.sort] ?? 'created_at';
The worst pattern is building the whole string with a template literal and passing it in with no parameters at all. A search for query calls that contain a template string with an interpolated variable will find most of them. Our post on ORM calls that bypass parameter binding lists patterns to search for in several libraries.
A rule for your team
- Prefer bind parameters for values.
- Allow replacements where the library or dialect makes bind awkward.
- Never concatenate request data into the SQL string.
- Use an allowlist for identifiers.
- Add a code search rule in CI for query calls with interpolated strings.
Safeguard scans dependencies, including Sequelize and database drivers, and policy gates can stop a pull request on a failed check. A free scan is at gold.safeguard.sh/scan.
Try it yourself
Run the code scan and open a query finding. Look for the difference between a value that is placed into the SQL string and one that is passed as a parameter.
Click through it at your own pace, or open it full screen.
Quick check
Can I use both in one query?
The Sequelize guide says a query can have either bind parameters or replacements.
Which is safer?
Bind parameters keep values out of the SQL text, so they remove the dependence on escaping.
Do they protect a column name in ORDER BY?
No. Map a fixed choice to a known identifier in code.
Sources
Last reviewed: 19 October 2026.