Skip to content

NOWAIT, lock timeouts, SKIP LOCKED

Waiting in a lock queue is the default, not the law. MySQL gives you three ways out: fail instantly, give up after a deadline, or pretend locked rows don't exist.

NOWAIT: fail fast

A> BEGIN;
Query OK

A> SELECT id FROM accounts WHERE id = 1 FOR UPDATE;
 id 
----
  1 
(1 row)

B> SELECT id FROM accounts WHERE id = 1 FOR UPDATE NOWAIT; -- ER_LOCK_NOWAIT — instantly, no waiting
ERROR 3572 (HY000): Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.

Once A is done, the same statement succeeds.

A> COMMIT;
Query OK

B> SELECT id FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
 id 
----
  1 
(1 row)

Verified against MySQL 8.4.11 · Run it yourself · Scenario source

innodb_lock_wait_timeout: wait, but not forever

Every InnoDB lock wait is already bounded by innodb_lock_wait_timeout, 50 seconds by default, settable per session (whole seconds only). When it fires you get errno 1205, and (easy to miss) it rolls back only the statement, not the transaction, which stays open and keeps every lock it already holds:

A
B
UPDATE (holds row lock)
UPDATE — waits 1s← 1205 Lock wait timeout exceeded
COMMIT
retry UPDATE→ 300
A> BEGIN;
Query OK

A> UPDATE accounts SET balance = 200 WHERE id = 1;
Query OK, 1 row affected

B> SET SESSION innodb_lock_wait_timeout = 1;
Query OK

B queues for the row lock like anyone else — but gives up after a second.

B> UPDATE accounts SET balance = 300 WHERE id = 1; -- ER_LOCK_WAIT_TIMEOUT, raised after the timeout
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

The failure canceled only B's statement — a retry after A commits works.

A> COMMIT;
Query OK

B> UPDATE accounts SET balance = 300 WHERE id = 1;
Query OK, 1 row affected

B> SELECT balance FROM accounts WHERE id = 1;
 balance 
---------
     300 
(1 row)

Verified against MySQL 8.4.11 · Run it yourself · Scenario source

1205 ≠ 1213

A deadlock (1213) rolls back your whole transaction; a lock timeout (1205) rolls back one statement. After 1205 your transaction is alive and still holding locks: either retry the statement or ROLLBACK, but don't assume you're back at a clean slate. (Set innodb_rollback_on_timeout=ON server-wide if you want 1205 to roll back the whole transaction.)

SKIP LOCKED: the job-queue primitive

A
B
C
D
grabs job 1
job 1 taken→ grabs job 2
grabs job 3
queue empty→ nothing
crash (rollback)→ job 1 freed
grabs job 1

Four workers run the exact same query at the same time.

A> BEGIN;
Query OK

A> SELECT * FROM jobs ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
 id |    task    
----+------------
  1 | send email 
(1 row)

B> BEGIN;
Query OK

B> SELECT * FROM jobs ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; -- job 1 is locked by A — skipped, no waiting
 id |     task     
----+--------------
  2 | resize image 
(1 row)

C> BEGIN;
Query OK

C> SELECT * FROM jobs ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
 id |     task     
----+--------------
  3 | build report 
(1 row)

Worker D finds the queue empty — an instant answer, not a wait.

D> SELECT * FROM jobs ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
Empty set

A worker crash (rollback) puts its job straight back on the queue.

A> ROLLBACK;
Query OK

D> SELECT * FROM jobs ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
 id |    task    
----+------------
  1 | send email 
(1 row)

B> COMMIT;
Query OK

C> COMMIT;
Query OK

Verified against MySQL 8.4.11 · Run it yourself · Scenario source

Three exits from the queue, three shapes: FOR UPDATE NOWAIT fails instantly with errno 3572, innodb_lock_wait_timeout gives up after its deadline with 1205, and FOR UPDATE SKIP LOCKED pretends the locked rows aren't there. That last one reads a deliberately inconsistent view: perfect for grabbing any free job, wrong for anything that must see every row. PostgreSQL offers the same NOWAIT and SKIP LOCKED syntax but a millisecond-granular lock_timeout and SQLSTATE 55P03 (compare). Row locks have been the whole story so far; the lock that takes down migrations is a table-level one.

Further reading

MIT Licensed · Every transcript on this site was generated by a real database run against MySQL 8.4.11 and PostgreSQL 18.4 at c52e5c9, and re-proven through psycopg and PyMySQL.