MY.L3.MISSING_LOCK_WAIT_TIMEOUT — A migration that waits for a metadata lock without a bound
- Category: safety
- Level: 3
- Confidence: deterministic
- Downtime class:
blocking - Stability: stable
- Suites: lint
- Applies to: MySQL 8.4
- Not transferable to MariaDB: this page describes MySQL 8.4 behavior. MariaDB answers the same driver and does not share these semantics, so SQLens refuses it outright rather than reasoning about it — see drivers/unsupported.
A schema change on an existing table has to take a metadata lock first. If somebody else is
holding one, your statement waits — and lock_wait_timeout decides for how long.
Its default is 31536000 seconds. A year. That is not a bound anybody chose; it is the value you get for never having thought about it.
The damage is not the waiting
MySQL grants metadata locks in request order. A migration stuck at the front of that queue does
not only delay itself: every statement that arrives afterwards and wants the same table queues behind
it, including plain SELECTs that would have been served instantly. One blocked ALTER TABLE can
therefore stall an application that was never writing to the table at all.
That ordering is why this is blocking even when the schema change itself is an online operation.
An ADD COLUMN that MySQL performs instantly is still an ADD COLUMN that had to get the lock.
Flagged
// No bound: if the table is busy, this waits for the metadata lock
// until it gets one — the default is a year.
Schema::table('orders', function (Blueprint $table) {
$table->text('note')->nullable();
});
Preferred
// Bound the wait first. MySQL has no SET LOCAL, so this is SET SESSION
// and stays for the rest of the connection.
DB::statement('SET SESSION lock_wait_timeout = 3');
Schema::table('orders', function (Blueprint $table) {
$table->text('note')->nullable();
});
The value is yours to choose and SQLens deliberately suggests none: a right one depends on how long the busiest transaction on that table runs and on how much deploy window you have, and neither of those is in the migration.
Two settings that look like the answer and are not
innodb_lock_wait_timeout bounds row locks — what a DELETE or an UPDATE waits for. A DDL
does not queue for those, so setting it does nothing for the wait this rule is about. SQLens does not
accept it in place of lock_wait_timeout.
max_execution_time is the one that looks like PostgreSQL's statement_timeout, and it is the
more expensive mistake. It is measured in milliseconds and applies to read-only SELECT
statements. Measured on MySQL 8.4.10: under SET max_execution_time=200, a query with real table
access was aborted after 235 ms with ERROR 3024, while the identical work wrapped in a DO (…) —
a non-SELECT — ran for 612 seconds and was never interrupted.
So a preamble offering it above an ALTER TABLE would promise a bound that does not reach the
statement below it. MySQL has no counterpart to statement_timeout for DDL; SQLens says so
rather than covering the gap with the nearest-sounding setting.
SET SESSION, not SET LOCAL
PostgreSQL scopes a preamble to the migration's own transaction with SET LOCAL. MySQL has no such
form: the value stays for the rest of the connection. On a deploy connection that is usually what you
want; on a pooled application connection it is usually not. Set it knowing that.
What the rule reports, and how often
One finding per migration, on the first statement that takes a metadata lock — not one per risky statement, which would bury the point under repetition of itself.
It stays quiet when the migration only touches tables it created itself: there is no queue to join for an object that did not exist a statement ago.
It reads the migration's own statements. A project that sets lock_wait_timeout in its
connection options, as a server default, or from a deploy wrapper is already bounded and is still
reported — a static reader sees the migration, not the connection.
Sources
- Server system variables — MySQL 8.4 Reference Manual
- Metadata locking — MySQL 8.4 Reference Manual
The fix material this rule carries
A finding from this rule carries machine-readable fix material, using this sequence:
timeout_preamble— Bound how long the statement may wait and how long it may run before it is given up on.
The payload is material for you or an agent to apply. SQLens writes no migration and runs no DDL. See the remediation payload for every field, the placeholder semantics, and the version rules a consumer has to follow.