Databases

Benchmarking Short vs Long Transactions in PostgreSQL Impact on Locks and Deadlocks

How holding locks for milliseconds longer can cripple your database- and how to prove it with a repeatable benchmark

Benchmarking Short vs Long Transactions in PostgreSQL Impact on Locks and Deadlocks

Every developer working with a database faces a fundamental design choice: how to structure their transactions. Should a complex business operation, involving multiple database writes, be wrapped in a single, all-or-nothing transaction? Or is it better to break it down into a series of smaller, faster commits? On the surface, the long_running_transaction seems safer, guaranteeing perfect atomicity. But as traffic grows, a mysterious performance degradation can set in. Latency spikes, throughput drops, and users start seeing errors. The culprit is often not the queries themselves, but the invisible contention they create.

This isn’t just a theoretical debate. The choice between a short_transaction and a long_running_transaction has a profound and measurable impact on your database’s health, particularly concerning lock contention and the deadlock_rate. Holding locks for even a few extra milliseconds can cascade into system-wide blocking_time, crippling your application under load. In this guide, we’ll move beyond theory and design a practical postgres transaction_benchmark using pgbench to demonstrate precisely how and why short, targeted transactions are crucial for building scalable, high-performance systems.

The Anatomy of a PostgreSQL Transaction: More Than Just a Query

To understand the performance implications, we must first understand what happens when you type BEGIN;. A PostgreSQL transaction is a resource-intensive event. It creates a “snapshot” of the database, ensuring a consistent view of the data for its duration. More importantly, as it modifies data, it acquires locks. These locks are the mechanism that ensures data integrity, preventing two transactions from overwriting the same row in a chaotic way.

  • A short_transaction acquires a lock, performs its update, and commits, releasing the lock within a few milliseconds. The window for another transaction to conflict with it is tiny. This is the principle behind an autocommit_test or high commit_frequency model.
  • A long_running_transaction acquires a lock and holds onto it. It might perform other queries, wait for application logic to complete, or even pause for an external API call. During this entire time, that lock is held. Any other transaction needing that same row must wait, creating write_contention. The longer the transaction, the higher the probability of a conflict.

Furthermore, long transactions have a sinister side effect on database maintenance. PostgreSQL’s VACUUM process cannot clean up “dead” row versions that are still visible to an open transaction. A single long_running_transaction can prevent VACUUM from reclaiming space on entire tables, leading to table bloat, slower queries, and a general decline in database performance over time.

Designing a Realistic Transaction Benchmark with pgbench

To make this tangible, we need a repeatable benchmark_script. PostgreSQL’s built-in tool, pgbench, is perfect for this. While it comes with a default TPC-B-like workload, its real power lies in custom scripts that can simulate your application’s specific access patterns.

Our goal is to create a scenario with high_write_tables—specifically, high contention on just a few rows, which is a common bottleneck in many applications (e.g., updating inventory for a popular product, or changing the status on a central configuration table).

Setting Up the Test Environment

First, we set up pgbench with a custom table that will be our point of contention. A dedicated database for the test is created, and then pgbench is initialized with a small scale factor. Finally, a custom products table is created to serve as the point of high contention, populated with just a couple of products with an inventory count.

The “Long Transaction” Benchmark Script

Our first script simulates a complex operation that updates two products within a single, long-running transaction. Crucially, it includes a short, 50-millisecond sleep between the two updates. This pause doesn’t represent a database-intensive operation; rather, it simulates application processing time, network latency for another service call, or any other work that holds the transaction open and its locks in place for longer than necessary.

The “Short Transaction” Benchmark Script

Our second script performs the same logical work—updating two products—but does so by committing each update immediately in its own separate transaction. This approach breaks the work into two distinct, atomic operations, minimizing the total time that any single lock is held.

Running the Benchmark and Analyzing the Results

Both custom scripts are then run using pgbench. The tool is configured with a high number of concurrent clients and worker threads to deliberately create a high-contention environment. Each test runs for a fixed duration, such as 60 seconds.

Key Metrics to Watch

While pgbench provides transaction throughput (TPS) as its main output, the real story is in the database’s internal state. This is where a real-time monitoring tool is indispensable.

  • Transaction Throughput (TPS): The raw performance number from pgbench.
  • Lock Waits: The number of processes stuck waiting for a lock to be released. This is the most direct measure of blocking_time.
  • Deadlocks: Tracking the cumulative number of deadlocks detected by the database.
  • Statement Performance: Identifying which specific queries are spending the most time executing versus waiting.

Visualizing Contention with Netdata

Instead of manually running queries, a tool like Netdata provides immediate, visual feedback. During the lock_wait_benchmark, the netdata_postgres collector automatically tracks and visualizes these critical metrics.

When running the “Long Transaction” test, a Netdata dashboard would light up:

  • The PostgreSQL Locks chart would show a dramatic and sustained spike in waiting processes.
  • The PostgreSQL Deadlocks chart, which tracks the cumulative count, would start ticking upwards.
  • The Transactions chart would show a high number of rollbacks, as PostgreSQL aborts transactions to resolve deadlocks.

In stark contrast, the “Short Transaction” test would show minimal lock waits and likely zero deadlocks, even with the same number of concurrent clients. The contention_results become visually undeniable.

Analyzing the Contention_Results

The results from this postgres_performance_test are typically night and day.

  • Short Transactions: Achieves a significantly higher TPS. The database remains healthy, with minimal blocking_time and virtually no serialization_failures or deadlocks.
  • Long Transactions: Throughput plummets. pgbench will report a very high average latency per transaction. The Netdata dashboard will confirm that this latency is not due to CPU or I/O load, but almost entirely due to processes waiting for locks. The deadlock_rate becomes a serious issue, causing work to be thrown away.

This benchmark proves a critical principle:total transaction time, not the number of queries, is the primary driver of lock contention.The 50ms sleep in our long transaction script was enough to create a traffic jam, where dozens of clients ended up waiting in a queue for locks held by others.

Final Recommendations

The evidence is clear. For building scalable applications that perform under pressure, managing transaction scope is paramount.

  1. Keep Transactions Short: A transaction should live for milliseconds, not seconds. Encapsulate the smallest possible logical unit of work.
  2. Separate Fetches from Writes: Never hold a transaction open while waiting for user input, application processing, or network calls. Fetch the data you need, close the transaction, perform your logic, and then start a new transaction to write the result.
  3. Batch Strategically: For large data imports or updates, break the work into smaller chunks of a few hundred or thousand rows, with a COMMIT after each chunk. This releases locks periodically and allows maintenance processes like VACUUM to do their work.

Understanding this behavior is the first step, but seeing it in your own environment is what truly drives change. To discover hidden long_running_transaction problems and visualize their impact on your database, you need comprehensive, real-time monitoring.

Netdata’s detailed PostgreSQL dashboards can help you pinpoint the exact queries and transactions causing lock contention in your production environment. Try it for free today.