Avoiding Long-Running Transactions in MySQL
AI generated
InnoDB
SQL
MySQL · InnoDB · Transactions · Monitoring
Avoiding Long-Running Transactions
before they slow down the entire system

A single transaction left open can make the InnoDB undo log grow without bound, hold locks for minutes, and delay replication, without a single error message appearing anywhere. This article explains how long-running transactions arise in MySQL, how to reliably track them down with information_schema.innodb_trx, and which timeout and batch strategies prevent them in application code from the start.

17 min read innodb_trx · Undo Log · Lock Wait Timeout MySQL 8.0 · InnoDB · PHP 8.x

1. Why Long Transactions Become a Problem

A long-running transaction is not a transaction with a fixed time limit, but any transaction that stays open significantly longer than the actual business operation requires. A report accidentally running inside the same transaction as an import, a forgotten COMMIT in an interactive session, or a batch job processing ten thousand rows in a single pass: all three produce the same class of problems, even though the cause differs each time.

What makes a long-running transaction so treacherous is that it initially produces no visible errors. The application keeps running, queries return results, and only after minutes or hours do the consequences appear: growing tablespace files, blocked concurrent writes, and replication that falls further and further behind. The following sections show how InnoDB is technically burdened by long transactions and which concrete tools and patterns bring the problem under control.

2. How InnoDB Organizes Undo Logs and Purge

InnoDB uses an undo log for multi-version concurrency control, keeping the previous version of a row for every change made to it. This undo log lets concurrent transactions read a consistent snapshot of the data thanks to MVCC, without being affected by write access from other transactions. A background process, the purge thread, cleans up undo log entries as soon as no active transaction still needs an older snapshot in which that entry would need to remain visible.

A single long-running transaction prevents exactly this cleanup process. As long as the old transaction stays open, InnoDB must keep every undo log entry that could be relevant to its snapshot, even if thousands of other transactions commit in the meantime. The result is a steadily growing history list length, visible via SHOW ENGINE INNODB STATUS, which bloats the tablespace and measurably degrades read speed for all subsequent queries, because InnoDB has to walk longer version chains.


-- Check current InnoDB history list length (undo log growth indicator)
SHOW ENGINE INNODB STATUS\G
-- Look for: "History list length NNNN" in the TRANSACTIONS section

-- Alternative: query via information_schema (MySQL 8.0.30+)
SELECT NAME, COUNT
FROM information_schema.INNODB_METRICS
WHERE NAME = 'trx_rseg_history_len';

3. Finding Long-Running Transactions with innodb_trx

The table information_schema.innodb_trx lists all currently open InnoDB transactions with their start time, state, and associated thread ID. Using the trx_started column, the runtime of each transaction can be calculated, so long-running transactions can be filtered out specifically instead of manually scrolling through the process list. Combined with information_schema.processlist, you additionally get the last executed SQL statement of the respective connection.

This query should be a fixed part of any monitoring setup that wants to proactively detect long-running transactions, rather than only reacting once users complain about timeouts. In production environments, a threshold of a few minutes is a reasonable starting point for flagging a transaction as suspicious, depending on the typical workload of the respective application.


-- Find transactions running longer than 60 seconds
SELECT
  trx_id,
  trx_state,
  trx_started,
  TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_seconds,
  trx_mysql_thread_id,
  trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY duration_seconds DESC;

-- Cross-reference with the full connection state
SELECT p.id, p.user, p.host, p.db, p.time, p.state, p.info
FROM information_schema.processlist p
JOIN information_schema.innodb_trx t ON t.trx_mysql_thread_id = p.id
WHERE t.trx_started < NOW() - INTERVAL 60 SECOND;

4. Analyzing Locks and Lock Waits with performance_schema

A long-running transaction is not only a problem for the undo log, it often also holds row locks that block other transactions. The performance_schema provides, through the tables data_locks and data_lock_waits, a detailed view of which transaction is waiting for a lock held by another transaction. In older MySQL versions, information_schema.innodb_lock_waits handled this task but was deprecated in favor of performance_schema.

Anyone who regularly finds blocked queries in the log should identify not just the blocked transaction but specifically the blocking transaction. Often that blocking transaction is exactly the long-running transaction that should have committed long ago. A targeted KILL of the blocking connection is acceptable as an emergency measure, but should always be followed by root cause analysis of why the transaction stayed open for so long.


-- Which transaction blocks which other transaction (MySQL 8.0+)
SELECT
  waiting_trx.trx_id AS waiting_trx_id,
  waiting_trx.trx_mysql_thread_id AS waiting_thread,
  blocking_trx.trx_id AS blocking_trx_id,
  blocking_trx.trx_mysql_thread_id AS blocking_thread,
  blocking_trx.trx_started AS blocking_since
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx waiting_trx
  ON waiting_trx.trx_id = w.requesting_engine_transaction_id
JOIN information_schema.innodb_trx blocking_trx
  ON blocking_trx.trx_id = w.blocking_engine_transaction_id;

5. Application-Side Causes of Unnecessarily Long Transactions

The most common cause of a long-running transaction is not in the database but in the application code. A classic pattern: a transaction is opened, then an external HTTP call follows, for example to a payment provider or an email service, and only after the response is committed. As long as the external service is responding, the database transaction stays open, even though the actual database operation would have finished long ago. External network calls should fundamentally never happen inside an open transaction.

A second common cause is ORM behavior that implicitly keeps a transaction open while the application code still runs expensive calculations or loops with many individual operations. Interactive database sessions from developers who start with START TRANSACTION and then debug for minutes before committing or rolling back are also, in practice, among the most common sources of production-damaging long-running transactions, especially in staging and test environments that accidentally share the same server as production.

6. Timeout Strategies: innodb_lock_wait_timeout and Application Logic

The server parameter innodb_lock_wait_timeout limits how long a transaction waits for a lock before it is aborted with an error. The default value of 50 seconds is too high for many web applications, because a user should not have to wait 50 seconds for a blocked page. A lower, application-specific setting, for example via SET SESSION innodb_lock_wait_timeout = 5, forces fast feedback and prevents blocked connections from piling up in the connection pool.

In addition to the plain lock wait timeout, the application layer itself should enforce a maximum transaction duration, for example through a watchdog that logs open transactions after a defined time and, in extreme cases, closes the connection. This dual safeguard, database parameters and application logic together, reliably prevents a single faulty long-running transaction from staying open unnoticed for hours.


-- Session-level lock wait timeout, tighter than the global default
SET SESSION innodb_lock_wait_timeout = 5;

-- Check current global and session values
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';

-- Global setting for the whole server (requires SUPER privilege)
SET GLOBAL innodb_lock_wait_timeout = 15;

7. Batch Processing: Splitting Up Large Transactions Sensibly

A bulk update or import that processes tens of thousands of rows in a single transaction is one of the most common self-inflicted long-running transactions. The solution is batching: the total amount is split into smaller chunks, each chunk runs in its own short transaction with its own COMMIT. This drastically reduces the maximum lock duration per transaction and keeps the undo log small, because purge can clean up regularly between chunks.

What matters in batch processing is idempotency: if the process is aborted after a partial failure, it must be safely resumable without processing already handled rows a second time. A progress marker, typically the last processed ID, makes the batch process robust against interruptions and replaces the false safety of one giant transaction with genuine resumability.


<?php
declare(strict_types=1);

/**
 * Processes a large dataset in small, short-lived transactions
 * instead of one long-running transaction.
 */
function batchUpdatePrices(PDO $pdo, int $batchSize = 500): void
{
    $lastId = 0;

    while (true) {
        $pdo->beginTransaction();

        $stmt = $pdo->prepare(
            'SELECT id, price FROM product
             WHERE id > :last_id
             ORDER BY id ASC
             LIMIT :batch_size
             FOR UPDATE'
        );
        $stmt->bindValue('last_id', $lastId, PDO::PARAM_INT);
        $stmt->bindValue('batch_size', $batchSize, PDO::PARAM_INT);
        $stmt->execute();
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        if (count($rows) === 0) {
            $pdo->commit();
            break; // no more rows left
        }

        $update = $pdo->prepare('UPDATE product SET price = price * 1.05 WHERE id = :id');
        foreach ($rows as $row) {
            $update->execute(['id' => $row['id']]);
            $lastId = (int) $row['id'];
        }

        $pdo->commit(); // short transaction, released immediately
    }
}

8. Monitoring and Alerting for Long-Running Transactions

Reactive debugging is not enough for long-running transactions, because the damage has already occurred by the time it is discovered. A monitoring job that queries information_schema.innodb_trx at regular intervals and alerts when a threshold is exceeded detects the problem before users notice it through timeouts. Tools like Percona Monitoring and Management or a simple cron job with email dispatch serve this purpose equally well.

In addition, the history list length from SHOW ENGINE INNODB STATUS should feed into a monitoring dashboard as a metric. A continuously rising value over several hours is a reliable early warning signal for a long-running transaction hanging somewhere, even if the specific transaction is not obviously long at the moment of observation but keeps recurring periodically and never fully clears.

9. Symptoms, Causes, and Solutions at a Glance

The following table maps typical symptoms of long-running transactions to their most common causes and the matching countermeasure, as a quick reference for troubleshooting in production.

Symptom Likely cause Countermeasure
Growing tablespace files Undo log cannot be purged Check innodb_trx for open transactions
Slow SELECT queries Long version chains from high history list length Identify the purge blocker and let it commit
Lock wait timeouts in the log External API call inside an open transaction Keep network calls strictly outside transactions
Replication lag Bulk update in a single transaction Batch processing with small transactions
Connection pool exhausted Default innodb_lock_wait_timeout too high Lower session timeout for the application

This mapping does not replace deeper diagnosis, but it gives you a quick starting point in an acute situation to move from an observed symptom to the most likely cause of a long-running transaction, before larger damage occurs.

10. Summary

Long-running transactions are dangerous because they initially stay unnoticed while, in the background, they bloat the undo log, hold locks, and delay replication. The table information_schema.innodb_trx is the first tool for making open transactions and their runtime visible, complemented by performance_schema.data_lock_waits for analyzing concrete blockages. The most common cause lies in application code: external network calls inside open transactions and bulk updates without batching.

Effective countermeasures combine database configuration and application design: a lower innodb_lock_wait_timeout, consistent batching of large data volumes into short transactions, and continuous monitoring of the history list length as an early warning system. Anyone who considers these three layers together prevents a single forgotten transaction from becoming a bottleneck for the entire system.

Avoiding long-running transactions, the essentials at a glance

Finding them

Regularly check information_schema.innodb_trx for high-runtime transactions, combined with the process list.

Avoiding causes

Never make external network calls inside an open transaction. Always split bulk updates into batches.

Setting timeouts

Set an application-specific innodb_lock_wait_timeout well below the global default of 50 seconds.

Monitoring

Track history list length as a metric on the monitoring dashboard, as an early warning signal for hanging transactions.

11. FAQ: Avoiding Long-Running Transactions

1What exactly is a long-running transaction?
Any transaction open longer than the business operation requires. Causes include forgotten COMMITs, API calls, or missing batching.
2Why does the undo log grow?
InnoDB must retain entries for the open transaction. Purge can only clean up after its COMMIT.
3How do I find them in MySQL?
Via information_schema.innodb_trx, filtered by runtime since trx_started, combined with processlist.
4What is the history list length?
Count of not yet purged undo log entries. A steady rise means a transaction is hanging somewhere.
5Why no API calls in transactions?
The database transaction stays open during network latency and unnecessarily holds locks.
6Which value for innodb_lock_wait_timeout?
3 to 10 seconds at the session level, well below the global default of 50 seconds.
7How do I split imports into batches?
Via a progress marker, fixed chunk sizes, and its own short transaction with COMMIT per chunk.
8Can I just kill it?
Yes, with KILL based on the thread ID, but always followed by root cause analysis.
9Does it affect replication?
Yes, bulk updates in one transaction can noticeably delay replicas.
10Is the process list alone enough?
No, only the combination of innodb_trx, processlist, and history list length gives a complete picture.