|
| 1 | +--- |
| 2 | +title: 'ERROR 1205 (HY000): Lock Wait Timeout Exceeded in MySQL' |
| 3 | +--- |
| 4 | + |
| 5 | +## Error Message |
| 6 | + |
| 7 | +```sql |
| 8 | +ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
| 9 | +``` |
| 10 | + |
| 11 | +## What Triggers This Error |
| 12 | + |
| 13 | +MySQL 1205 fires when a transaction waits longer than `innodb_lock_wait_timeout` seconds (default: 50) to acquire a row lock held by another transaction. Unlike a deadlock (ERROR 1213), MySQL does not automatically detect this — it simply gives up waiting. The fix depends on why the lock is held so long: |
| 14 | + |
| 15 | +- **Long-running transaction holding locks** — an uncommitted transaction keeps row locks open |
| 16 | +- **Bulk UPDATE or DELETE blocking other transactions** — a large write locks thousands of rows |
| 17 | +- **Foreign key checks causing implicit locks on the parent table** — InnoDB reads the parent row with a shared lock |
| 18 | +- **`innodb_lock_wait_timeout` too low for batch operations** — the default 50s isn't enough for heavy workloads |
| 19 | +- **Circular wait that wasn't detected as a deadlock** — rare edge case where the wait graph check missed the cycle |
| 20 | + |
| 21 | +## Fix by Scenario |
| 22 | + |
| 23 | +### Long-running transaction holding locks |
| 24 | + |
| 25 | +The most common cause. A transaction ran a `SELECT ... FOR UPDATE` or an `UPDATE`, then never committed — maybe the application crashed, a developer left a session open, or a retry loop is stuck. |
| 26 | + |
| 27 | +```sql |
| 28 | +-- Find the blocking transaction |
| 29 | +SELECT |
| 30 | + r.trx_id AS waiting_trx_id, |
| 31 | + r.trx_mysql_thread_id AS waiting_thread, |
| 32 | + r.trx_query AS waiting_query, |
| 33 | + b.trx_id AS blocking_trx_id, |
| 34 | + b.trx_mysql_thread_id AS blocking_thread, |
| 35 | + b.trx_query AS blocking_query, |
| 36 | + b.trx_started AS blocking_since |
| 37 | +FROM information_schema.INNODB_LOCK_WAITS w |
| 38 | +JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id |
| 39 | +JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id; |
| 40 | +``` |
| 41 | + |
| 42 | +For MySQL 8.0+, use the `performance_schema` instead: |
| 43 | + |
| 44 | +```sql |
| 45 | +SELECT |
| 46 | + waiting.THREAD_ID AS waiting_thread, |
| 47 | + waiting.SQL_TEXT AS waiting_query, |
| 48 | + blocking.THREAD_ID AS blocking_thread, |
| 49 | + blocking.SQL_TEXT AS blocking_query |
| 50 | +FROM performance_schema.data_lock_waits w |
| 51 | +JOIN performance_schema.events_statements_current waiting |
| 52 | + ON waiting.THREAD_ID = w.REQUESTING_THREAD_ID |
| 53 | +JOIN performance_schema.events_statements_current blocking |
| 54 | + ON blocking.THREAD_ID = w.BLOCKING_THREAD_ID; |
| 55 | +``` |
| 56 | + |
| 57 | +**Fix:** |
| 58 | + |
| 59 | +1. Kill the blocking session if it's idle or stuck: |
| 60 | + |
| 61 | +```sql |
| 62 | +-- Check if the blocking thread is doing anything |
| 63 | +SHOW PROCESSLIST; |
| 64 | + |
| 65 | +-- Kill the idle blocker (use the blocking_thread from above) |
| 66 | +KILL 12345; |
| 67 | +``` |
| 68 | + |
| 69 | +2. Fix the application to commit or rollback promptly: |
| 70 | + |
| 71 | +```python |
| 72 | +# Bad: connection stays open with uncommitted transaction |
| 73 | +cursor.execute("UPDATE orders SET status = 'processing' WHERE id = %s", (order_id,)) |
| 74 | +result = call_payment_api(order_id) # 60 seconds — locks held the entire time |
| 75 | +cursor.execute("UPDATE orders SET status = %s WHERE id = %s", (result, order_id)) |
| 76 | +connection.commit() |
| 77 | + |
| 78 | +# Good: separate transactions |
| 79 | +cursor.execute("UPDATE orders SET status = 'processing' WHERE id = %s", (order_id,)) |
| 80 | +connection.commit() # release locks immediately |
| 81 | + |
| 82 | +result = call_payment_api(order_id) # locks are free |
| 83 | + |
| 84 | +cursor.execute("UPDATE orders SET status = %s WHERE id = %s", (result, order_id)) |
| 85 | +connection.commit() |
| 86 | +``` |
| 87 | + |
| 88 | +### Bulk UPDATE or DELETE blocking other transactions |
| 89 | + |
| 90 | +A single `UPDATE` or `DELETE` affecting thousands of rows locks them all for the duration of the statement. Other transactions waiting for any of those rows will time out. |
| 91 | + |
| 92 | +```sql |
| 93 | +-- This locks the rows matched by the WHERE clause for the duration of the statement |
| 94 | +UPDATE orders SET status = 'archived' WHERE created_at < '2025-01-01'; |
| 95 | +-- Could take minutes — other transactions touching those rows may wait |
| 96 | +``` |
| 97 | + |
| 98 | +**Fix:** Break the operation into smaller batches in application code: |
| 99 | + |
| 100 | +```python |
| 101 | +batch_size = 1000 |
| 102 | +while True: |
| 103 | + cursor.execute(""" |
| 104 | + UPDATE orders SET status = 'archived' |
| 105 | + WHERE created_at < '2025-01-01' AND status != 'archived' |
| 106 | + LIMIT %s |
| 107 | + """, (batch_size,)) |
| 108 | + connection.commit() |
| 109 | + if cursor.rowcount == 0: |
| 110 | + break |
| 111 | + time.sleep(0.1) # let other transactions acquire locks |
| 112 | +``` |
| 113 | + |
| 114 | +### Foreign key checks causing implicit locks on parent table |
| 115 | + |
| 116 | +When you INSERT into a child table with a foreign key, InnoDB places a shared lock on the parent row to verify it exists. If another transaction holds an exclusive lock on that parent row, the child INSERT waits. |
| 117 | + |
| 118 | +```sql |
| 119 | +-- Transaction A: updates a customer (holds exclusive lock on id=42) |
| 120 | +START TRANSACTION; |
| 121 | +UPDATE customers SET name = 'New Name' WHERE id = 42; |
| 122 | +-- Does NOT commit yet |
| 123 | + |
| 124 | +-- Transaction B: inserts an order for that customer (needs shared lock on customers.id=42) |
| 125 | +INSERT INTO orders (customer_id, total) VALUES (42, 99.99); |
| 126 | +-- Waits... and eventually ERROR 1205 |
| 127 | +``` |
| 128 | + |
| 129 | +**Fix:** |
| 130 | + |
| 131 | +1. Keep the parent update transaction short — commit before the child insert needs the row |
| 132 | +2. If the parent update is part of a batch, process it in smaller chunks |
| 133 | +3. If FK validation isn't needed during bulk inserts, temporarily disable it: |
| 134 | + |
| 135 | +```sql |
| 136 | +-- Only for controlled batch operations — not for regular application use |
| 137 | +SET FOREIGN_KEY_CHECKS = 0; |
| 138 | +-- ... bulk inserts ... |
| 139 | +SET FOREIGN_KEY_CHECKS = 1; |
| 140 | +``` |
| 141 | + |
| 142 | +### `innodb_lock_wait_timeout` set too low |
| 143 | + |
| 144 | +The default is 50 seconds, which is usually enough. But batch jobs, reporting queries, or migration scripts may legitimately need longer. |
| 145 | + |
| 146 | +```sql |
| 147 | +-- Check the current timeout |
| 148 | +SELECT @@innodb_lock_wait_timeout; |
| 149 | + |
| 150 | +-- Increase for the current session only (for a batch job) |
| 151 | +SET SESSION innodb_lock_wait_timeout = 300; -- 5 minutes |
| 152 | + |
| 153 | +-- Or increase globally (requires careful consideration) |
| 154 | +SET GLOBAL innodb_lock_wait_timeout = 120; |
| 155 | +``` |
| 156 | + |
| 157 | +**Fix:** Set it per-session for batch operations rather than changing the global default. A high global timeout means genuine lock problems take longer to surface. |
| 158 | + |
| 159 | +### Circular wait not detected as deadlock |
| 160 | + |
| 161 | +Rarely, InnoDB's deadlock detector misses a cycle — especially with complex multi-table lock chains or when `innodb_deadlock_detect` is disabled (some high-concurrency setups turn it off for performance). |
| 162 | + |
| 163 | +```sql |
| 164 | +-- Check if deadlock detection is enabled |
| 165 | +SELECT @@innodb_deadlock_detect; |
| 166 | + |
| 167 | +-- Check the latest detected deadlock |
| 168 | +SHOW ENGINE INNODB STATUS; |
| 169 | +-- Look for the "LATEST DETECTED DEADLOCK" section |
| 170 | +``` |
| 171 | + |
| 172 | +**Fix:** |
| 173 | + |
| 174 | +1. Re-enable deadlock detection if it was turned off: |
| 175 | + |
| 176 | +```sql |
| 177 | +SET GLOBAL innodb_deadlock_detect = ON; |
| 178 | +``` |
| 179 | + |
| 180 | +2. Add application-level retry logic for 1205 errors (same pattern as deadlock retries): |
| 181 | + |
| 182 | +```python |
| 183 | +max_retries = 3 |
| 184 | +for attempt in range(max_retries): |
| 185 | + try: |
| 186 | + cursor.execute("START TRANSACTION") |
| 187 | + cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1") |
| 188 | + cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2") |
| 189 | + connection.commit() |
| 190 | + break |
| 191 | + except mysql.connector.Error as err: |
| 192 | + connection.rollback() |
| 193 | + if err.errno in (1205, 1213) and attempt < max_retries - 1: |
| 194 | + time.sleep(2 ** attempt) |
| 195 | + else: |
| 196 | + raise |
| 197 | +``` |
| 198 | + |
| 199 | +## Prevention |
| 200 | + |
| 201 | +- Commit transactions as quickly as possible — never hold locks while waiting for external API calls or user input |
| 202 | +- Break large UPDATE/DELETE operations into batches of 1000-5000 rows |
| 203 | +- Add indexes on columns used in WHERE clauses to avoid full table scans that escalate lock scope |
| 204 | +- Use `SET SESSION innodb_lock_wait_timeout` for batch jobs instead of raising the global default |
| 205 | +- Monitor `INNODB_TRX` for transactions running longer than expected and alert on them |
| 206 | +- Always implement retry logic for 1205 and 1213 errors in application code |
| 207 | + |
| 208 | +<HintBlock type="info"> |
| 209 | + |
| 210 | +Bytebase's [SQL Review](https://docs.bytebase.com/sql-review/review-rules/) can flag large UPDATE/DELETE statements without LIMIT during change review, preventing bulk operations from causing lock contention. See also [ERROR 1213: Deadlock Found](/reference/mysql/error/1213-deadlock-found) for deadlock-specific troubleshooting. |
| 211 | + |
| 212 | +</HintBlock> |
0 commit comments