This page is PostgreSQL and MySQL only.
Row locks
Control concurrent row access with FOR UPDATE, FOR SHARE, skipLocked, noWait, table targeting, and transaction-only enforcement.
Use a row lock when a read decides a write that must not race another transaction: claiming a job, reserving stock, moving money. The lock option on every read helper wraps Drizzle's FOR UPDATE, FOR SHARE, and related clauses.
Locks are supported on PostgreSQL and MySQL. SQLite has no row-level locking and throws LOCK_NOT_SUPPORTED. For complete patterns (job queues, cursor pages, includes, lock errors), see locking patterns.
Lock rows in a read
The simplest form is a string shorthand:
const users = await client.users.findMany({
where: { active: true },
lock: 'update',
});This generates SELECT ... FOR UPDATE.
The object form gives access to every option:
const users = await client.users.findMany({
where: { active: true },
lock: {
mode: 'update',
skipLocked: true,
},
});skipLocked and noWait
These control what happens when a locked row is already held by another transaction:
skipLocked: true- skip rows that are locked, return only unlocked rowsnoWait: true- fail immediately if any requested row is locked
They are mutually exclusive. Enabling both throws an error.
// Skip locked rows
const available = await client.jobs.findMany({
where: { status: 'pending' },
lock: {
mode: 'update',
skipLocked: true,
},
});With noWait, the query fails right away instead of waiting for the lock:
// Fail fast if locked
try {
const row = await client.jobs.findFirst({
where: { id: 1 },
lock: {
mode: 'update',
noWait: true,
},
});
} catch (error) {
// LOCK_TIMEOUT - the row was locked by another transaction
}Lock specific tables (PostgreSQL only)
On PostgreSQL you can scope the lock to specific tables using tables.
This is useful in joins where you only want to lock certain tables:
const posts = await client.posts.findMany({
where: { published: true, author: { is: { active: true } } },
include: { author: true },
lock: {
mode: 'update',
tables: ['posts'],
},
});tables accepts both TypeScript table keys and database table names.
Duplicate table references (by database name) are deduplicated automatically.
PostgreSQL only
tables is only supported on PostgreSQL. Using it on MySQL or SQLite throws
LOCK_NOT_SUPPORTED.
Enforcing transaction-only locks
Outside a transaction, the lock is released as soon as the statement finishes, so it protects nothing. You can enforce that locks only run inside explicit transactions:
const client = better(db, {
locks: {
transactionsOnly: true,
},
});With this config, any lock outside client.transaction(...) throws LOCK_REQUIRES_TRANSACTION:
// This throws
const users = await client.users.findMany({
lock: 'update',
});
// This works
await client.transaction(async (tx) => {
return tx.users.findMany({
lock: 'update',
});
});Reference
Lock modes
| Mode | SQL (PostgreSQL) | SQL (MySQL) |
|---|---|---|
'update' | FOR UPDATE | FOR UPDATE |
'share' | FOR SHARE | FOR SHARE |
'noKeyUpdate' | FOR NO KEY UPDATE | not supported |
'keyShare' | FOR KEY SHARE | not supported |
noKeyUpdate and keyShare are PostgreSQL-specific.
Using them on MySQL throws LOCK_NOT_SUPPORTED.
Methods that accept lock
Locks work on every read helper that accepts QueryArgs:
| Method | Accepts lock |
|---|---|
findMany | Yes |
findFirst | Yes |
findOne | Yes |
findUnique | Yes |
paginate | Yes |
cursor | Yes |
count | No |
exists | No |
| all write operations | No |
Dialect support
| Feature | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
lock: 'update' | Yes | Yes | rejected |
lock: 'share' | Yes | Yes | rejected |
lock: 'noKeyUpdate' | Yes | rejected | rejected |
lock: 'keyShare' | Yes | rejected | rejected |
skipLocked | Yes | Yes | N/A |
noWait | Yes | Yes | N/A |
tables | Yes | rejected | N/A |
transactionsOnly | Yes | Yes | N/A |