Skip to main content

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

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.