How InnoDB Auto-Increment Locking Works in MySQL
When MySQL’s InnoDB storage engine inserts rows into a table with an AUTO_INCREMENT column, it assigns generated numbers and coordinates concurrent inserts…
When PostgreSQL or MySQL serves many clients, its open-connection count does not show how much work it can handle. Some clients are idle, some run queries, and others wait for transactions to release locks. Concurrency scaling means handling more work at the same time without unacceptable increases in response time, failed transactions, or resource use.
Both databases use multiversion concurrency control (MVCC), which lets readers see a version of data while other transactions change it. Both also use locks to coordinate changes. Neither has a universal concurrency advantage: results depend on the application’s transactions, indexes, isolation settings, connection management, and hardware. The practical comparison is how each handles your workload, not which accepts the most connections.
PostgreSQL creates a separate server process for each client connection. Its documented max_connections default is typically 100, and raising the limit increases resource allocation, including shared memory. That setting controls how many connections the server accepts; it does not guarantee that the server can run that many queries efficiently. See the PostgreSQL connection documentation and connection settings.
MySQL 8.4’s default connection model assigns a thread to each client connection, with a thread cache that can reuse inactive threads. MySQL Enterprise Edition also offers a server-side thread pool, which manages statement execution without keeping a dedicated execution thread for every connection. That feature is specific to Enterprise Edition, so do not assume it is available in every MySQL installation. The MySQL connection documentation describes these models.
A connection pool lets a smaller number of database connections serve more clients. For PostgreSQL, PgBouncer can assign a server connection for a whole client session or only while a transaction runs. Transaction pooling reduces the need for server connections that stay open between transactions, but it limits behavior that depends on a session staying open. Check PgBouncer’s feature compatibility details before using that mode.
Track connected clients separately from statements that are actively executing. Also track transactions waiting on locks. Pooling addresses connection pressure. It does not make a database execute more work when its CPU, storage, or lock contention is already limiting throughput.
PostgreSQL and MySQL both combine MVCC with locks, but their default transaction isolation levels differ. Isolation level controls which committed changes a transaction can see and what coordination the database uses to protect that view.
PostgreSQL defaults to READ COMMITTED. Each statement sees rows committed before that statement began. InnoDB, MySQL’s storage engine, defaults to REPEATABLE READ, which gives a transaction a consistent view across its reads. The defaults are documented in PostgreSQL’s transaction settings and MySQL’s InnoDB isolation-level documentation.
InnoDB can use next-key locks for searches and index scans under REPEATABLE READ. A next-key lock protects an index record and the gap before it. This prevents certain inserts that would make a new row appear in a later read of the same search. The lock can also make an insert wait when another transaction has locked that part of the index. In the documented case, a lookup using a unique index to find one row avoids gap locking.
For example, an update that searches a broad range can involve more index records than one that finds a single row through a suitable index. When a transaction locks more records, other transactions have more chances to get stuck waiting. Before changing isolation levels to improve throughput, verify that the application still gets the consistency it needs. MySQL’s READ COMMITTED changes both which data a transaction sees and how InnoDB uses gap locks, so a change made only for a benchmark can alter application results.
When transactions update the same row, they must take turns changing it. A pooler or a higher connection limit cannot remove that conflict. Transactions that hold locks for a long time, scan unnecessary rows, or update shared rows in inconsistent orders also increase waiting and deadlock risk.
A deadlock occurs when transactions wait on one another in a cycle. InnoDB detects deadlocks by default and rolls back one transaction to break the cycle, so the application should be ready to retry it. PostgreSQL also reports deadlocks. At higher isolation levels, it can reject transactions that conflict with other transactions; applications may need to handle these serialization failures. See MySQL’s guidance on deadlocks and minimizing and handling them, alongside PostgreSQL’s serialization failure guidance.
Start by finding the transactions that wait the longest. Check whether they hold locks while doing unrelated work, scan unnecessary rows, or acquire locks in different orders. Shorter transactions and indexes that limit the rows scanned help reduce contention in either database.
MVCC also requires cleanup. PostgreSQL vacuuming reclaims obsolete row versions when they are no longer needed; long-running transactions can delay that work. InnoDB’s purge process removes old row and index information after it is no longer needed. The mechanisms and monitoring signals differ, so assess PostgreSQL vacuum health and InnoDB purge activity using each database’s own documentation: PostgreSQL vacuuming and InnoDB purge configuration.
No universal throughput winner follows from these architectural differences. A fair comparison uses the same mix of transactions, data, indexes, hardware, durability requirements (whether committed data must survive a failure), and isolation rules. It also uses equivalent connection management. Comparing PostgreSQL behind a pooler with MySQL without one measures both the database and the connection setup.
Increase the number of simultaneous requests gradually while keeping the workload realistic. Include the actual read/write mix, transaction size, and distribution of frequently updated records. A test that updates one shared row mainly measures contention around that row.
Record throughput along with response-time percentiles, lock waits, retries or failed transactions, CPU, memory, and storage activity. If throughput levels off while response times and retries rise, the database is no longer handling added concurrent work efficiently, even if it continues accepting connections. Test pooling separately from the database engines, and disclose when a test uses MySQL Enterprise Thread Pool.
Use a shared application workload or replay for a cross-database comparison. PostgreSQL’s pgbench measures its configured PostgreSQL workload; its result alone does not establish how MySQL would perform on the same application transactions.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com