What the hang looks like from the outside
Reading SHOW FULL PROCESSLIST without getting misled
SHOW FULL PROCESSLIST;. On a VPS that usually means the root database user. On shared hosting, a regular database user typically sees only its own connections. That is often enough here, because the blocking connection usually belongs to the same site.
TEXT
Illustrative output format (trimmed, IDs and table names are examples)
Id User Command Time State Info
4120 wp_user Sleep 1800 NULL
4388 wp_user Query 95 Waiting for table metadata lock ALTER TABLE wp_example_log ADD COLUMN ...
4391 wp_user Query 93 Waiting for table metadata lock SELECT * FROM wp_example_log WHERE ...
4392 wp_user Query 91 Waiting for table metadata lock SELECT * FROM wp_example_log WHERE ...
4397 wp_user Query 88 Waiting for table metadata lock INSERT INTO wp_example_log ...Sleep with a large Time value and no query attached. It looks harmless. It is usually the one holding the lock.
Why one idle connection can block an ALTER
lock_wait_timeout variable controls how long a statement waits for a metadata lock, and its default is very long on common MySQL and MariaDB versions. The exact value depends on the server and version. In practice, the ALTER will usually not give up before your visitors do.
Common holders of the shared lock include:
- A script or cron job that disabled autocommit and never committed.
- A hung PHP process with a persistent connection.
- A backup job partway through a dump.
- A long reporting query.
- A database GUI session someone left open with a transaction running.
Finding the connection that holds the lock
SQL
-- Read-only: lists open InnoDB transactions, oldest first
SELECT t.trx_mysql_thread_id AS thread_id,
t.trx_started,
TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_sec,
p.user, p.host, p.db, p.command, p.time,
t.trx_query
FROM information_schema.INNODB_TRX t
JOIN information_schema.PROCESSLIST p ON p.id = t.trx_mysql_thread_id
ORDER BY t.trx_started;command is Sleep, and whose trx_query is NULL. That combination describes a connection that opened a transaction, did some work, and has been idle with the transaction open ever since. The user and host columns tell you which application or tool owns it. That matters for the next step.
If the holder is not an InnoDB transaction, this query will not show it. Examples include a long query on a MyISAM table or an explicit LOCK TABLES. Go back to the process list and look for the oldest thread, older than the ALTER, that is working on or has touched the same table.
Some servers can show the lock directly. On MySQL, performance_schema.metadata_locks and the sys.schema_table_lock_waits view list who holds and who waits for each metadata lock, provided the instrumentation is enabled. On MariaDB, the equivalent is the METADATA_LOCK_INFO plugin, which is not always installed. These views are worth checking if they exist on your server, but do not count on them being there.
Deciding what is safe to kill
KILL <id> ends that connection, and the server rolls back its open transaction. KILL QUERY <id> will not help here. It stops only the statement currently running, and an idle connection is not running one. Before you kill the holder, check who owns it. Ending a forgotten GUI session or a stuck cron connection is usually safe. Ending a backup is safe for the database, but the backup run will be incomplete. Ending a connection in the middle of a large write is the risky case, because the rollback itself can take a while and holds its locks until it finishes.
The second lever is to kill the waiting ALTER. The queue clears immediately, because the request that blocked everyone disappears. The ALTER has not changed anything yet, since it was still waiting. The catch is on the WordPress side. The plugin files may already be the new version while the database schema is still the old one. Depending on the plugin, it might retry the migration on the next admin load, show a database update prompt, or simply break until someone reruns its upgrade routine. Check the plugin's behavior and run the update again once the holder is gone.
SQL
-- Thread IDs are illustrative; use the ones from your own output
KILL 4120; -- ends the idle connection holding the lock; its open transaction is rolled back
-- KILL 4388; -- alternative: drop only the waiting ALTER and retry the update later
-- Before running a schema change by hand, cap how long it may wait for the lock
SET SESSION lock_wait_timeout = 30;Confirming the fix and keeping it from returning
SHOW FULL PROCESSLIST; again. If you killed the holder, the ALTER should now be running or finished, and the waiting queries should drain within seconds. If you killed the ALTER, the waiting states should be gone, but the schema change still has not happened. Use SHOW CREATE TABLE on the affected table to confirm whether the new column or index exists. Then check that the plugin reports its database as up to date.
For prevention, run large plugin updates during quiet hours. Glance at the transaction query above before updating anything that is likely to alter a big table. Fix the root cause behind long-lived idle transactions: a script that never commits, a tool left open, or connection settings that keep sessions alive far longer than needed. If a site is slow but the process list shows no lock waits at all, the cause is elsewhere. Why WordPress Admin Is Slow Even When the Frontend Loads Fast covers the usual suspects in that case.
The practical rule: when a site hangs during an update and the process list fills with Waiting for table metadata lock, do not chase the queued queries or the plugin files. Find the oldest open transaction on that table and see who owns it. Then decide whether to end that connection or the waiting ALTER, based on what each one was doing.