Skip to content

Commit 14e2f1f

Browse files
docs: add 1205 1366 22p02 error code (#1073)
* add 1205 1366 22p02 error code * fix
1 parent f506969 commit 14e2f1f

3 files changed

Lines changed: 685 additions & 0 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

Comments
 (0)