Bali, Indonesia
Insights

Site Hangs During a Plugin Update? Look for Waiting for Table Metadata Lock

October 29, 2026
0:00
You click Update on a plugin, the spinner keeps turning, and a minute later the whole site stops responding. Pages do not throw an error. They hang. When you finally get into the database and list the running queries, dozens of them show the same state: Waiting for table metadata lock. The update itself is not slow. It is stuck behind something else, and now the rest of the site is stuck behind the update.
The pattern is consistent enough to recognize quickly. Front-end pages and wp-admin both stall. PHP workers fill up because every request is waiting on the database. Eventually the web server or reverse proxy starts returning gateway timeouts. CPU usage is often low, which throws people off. They expect a struggling server to be busy, but this one is mostly idle and waiting. The usual first reaction from site owners, and sometimes from junior support staff, is to blame the plugin and roll it back. That can make things worse. Restoring files does nothing for a database lock, and a second update attempt can queue another schema change behind the first one. Look at the database before you touch any files.
Connect to MySQL or MariaDB as a user that can see all threads and run 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 ...
This list contains two red herrings. The first is the pile of SELECT and INSERT queries. They look like the problem because there are so many of them, but they are victims. Killing them buys nothing, since every new page load adds more. The second is the ALTER TABLE. It shows the same waiting state, so it looks like one more victim. It is the reason the queue exists, but it is waiting too. The real cause is often the quietest line: a connection in Sleep with a large Time value and no query attached. It looks harmless. It is usually the one holding the lock.
A metadata lock protects a table's structure, not its rows. Whenever a transaction touches a table, the server gives that transaction a shared metadata lock on the table. The transaction keeps the lock until it commits or rolls back, not just until its statement finishes. A connection that ran one SELECT inside a transaction and then went idle without committing still holds that shared lock. ALTER TABLE needs an exclusive metadata lock, because it changes the structure every other query depends on. It cannot get that lock while anyone holds a shared one, so it waits. Waiting is not the damaging part. The damaging part is that a pending exclusive request takes priority over new shared requests. Every query that arrives after the ALTER and wants the same table lines up behind it, including simple reads. One forgotten transaction plus one ALTER is enough to freeze every page that reads that table. Online DDL does not avoid this. Even when a schema change can run in place without blocking reads and writes during the copy phase, it still needs the exclusive metadata lock briefly at the start and at the end. The same queue forms at those moments. The wait can also last a very long time. The 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.
For InnoDB tables, which is what most WordPress sites use, the fastest path is to list open transactions, oldest first, together with their connection details:
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;
Look for a row whose transaction started before the ALTER appeared, whose 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.
You have two levers, and they have different consequences. The first lever is to kill the holder. 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;
A blunt option is to restart the database service. Avoid it as a first move. It ends every connection on the server, rolls back every open transaction, and on a busy shared server it affects far more than one site. That is a much bigger blast radius than killing a single thread you have already identified.
Run 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.