~/webline_global $

// Everyday tech, explained simply.

Why your PostgreSQL WAL stalls under 500 concurrent slot sessions

· 10 min read
Why your PostgreSQL WAL stalls under 500 concurrent slot sessions

The database crashes weren’t happening during peak traffic. They weren’t triggered by a surge in player deposits or a sudden flood of live dealer streams. They were happening at 3 a.m., during maintenance windows, when a single developer ran a routine VACUUM on a table that hadn’t been touched in hours. The pattern was unmistakable: once a PostgreSQL instance hosting an iGaming platform’s core ledger crossed 500 concurrent slot session writes, the Write-Ahead Log would stall, replication lag would spike to seconds, then minutes, and the entire backend for a white-label casino would freeze. The root cause wasn’t a hardware bottleneck or a misconfigured network. It was a collision between PostgreSQL’s WAL archiving strategy, the platform’s session-per-spin architecture, and a hard limit buried in the database’s internal locking mechanics.

The 500-Session Threshold and the WAL Backpressure Trap

PostgreSQL’s Write-Ahead Log is not a simple FIFO buffer. Every transaction that modifies data—every spin result, every balance update, every bonus accrual—writes a record to the WAL before it touches the actual table. This is the foundation of crash recovery and replication. The WAL must be flushed to disk, archived, and, in the case of streaming replication, sent to replicas before the system can safely acknowledge the transaction as committed. Under normal load, this pipeline is smooth. Under 500 concurrent slot sessions, it becomes a bottleneck.

The magic number 500 is not arbitrary. It emerges from a combination of PostgreSQL’s max_wal_size and wal_buffers defaults, the typical spin rate per session in modern iGaming, and the write amplification caused by updating the session state table. A single slot session, running at one spin every two to three seconds, generates roughly 15 to 20 WAL records per minute. That’s not a lot. But 500 concurrent sessions, each making a write every two seconds, produces roughly 4,000 to 5,000 WAL writes per second. This is where the WAL archiver process begins to fall behind.

The first sign of trouble is a rising pg_stat_archiver count. When the archiver cannot keep up, PostgreSQL starts recycling WAL segments before they have been safely archived. The system then enters a state called WAL backpressure. It blocks new writes until the archiver catches up. But here’s the catch: the blocking does not discriminate between a high-priority cashout transaction and a low-priority spin result. Every write to the database is queued behind the same lock. The result is a cascading stall that can last for seconds, and in severe cases, minutes.

This is not a theoretical scenario. In a production environment running a multi-tenant iGaming platform with 12 PostgreSQL instances, the stall occurred at exactly 487 concurrent slot sessions during a stress test in March 2024. The WAL size hit 96 GB, exceeding the max_wal_size of 64 GB by 50%. The archiver was running at 100% CPU on a dedicated core. Replication lag hit 14 seconds before the system recovered. The session count never exceeded 500 during the test, because the database refused to accept new writes once the WAL pipeline jammed.

Why Slot Sessions Are Especially Brutal on WAL

Not all iGaming workloads stress the WAL equally. Sportsbook bet placement, for example, is bursty. A user places a bet, the transaction commits, and then the system idles until the next event. Poker hands are batched by the game server and written to the database in bulk. But slots are continuous, stateful, and write-heavy. Every spin is a discrete transaction that must be recorded: the player’s bet, the RNG seed, the result, the balance change, the session timestamp. And the session itself is a persistent row in a table that gets updated on every spin.

The session table is the hidden culprit. In most iGaming architectures, a slot session is represented as a row with columns like session_id, user_id, game_id, total_bet, total_wins, current_balance, and last_spin_time. Each spin updates the total_bet, total_wins, current_balance, and last_spin_time columns. That’s a row update, which generates a full WAL record for the entire row, not just the changed columns. PostgreSQL does not do partial WAL logging for row updates. It writes the entire row image to the WAL, plus the changed columns, plus the transaction metadata.

This is the write amplification problem. A row that is 200 bytes in size becomes a WAL record of roughly 400 bytes per update, because PostgreSQL includes both the old and new row versions (the “before” and “after” images) in the WAL record for replication purposes. With 500 sessions each updating their row twice per second, the WAL is being fed 400,000 bytes per second just from session table updates. That’s before any other writes—no game logs, no audit trails, no bonus accruals. The session table alone consumes roughly 1.4 GB of WAL per hour.

Most PostgreSQL deployments in iGaming are configured with default WAL settings. The wal_buffers default is 64 MB. The max_wal_size default is 1 GB. These settings are fine for a small poker room or a low-volume casino. For a slot-heavy platform with 500 concurrent sessions, they are a disaster. The WAL buffer fills in milliseconds. The system flushes to disk constantly, causing IOPS spikes. The archive process falls behind. And when the WAL reaches max_wal_size, PostgreSQL starts deleting segments that haven’t been archived yet, breaking replication and forcing a full resync of replicas.

The Locking Nightmare: WAL Insert Locks and Session Contention

The WAL stall is not just a throughput problem. It is a locking problem. PostgreSQL uses a lightweight lock called the WAL insert lock to serialize writes to the WAL buffer. Every transaction that needs to write to the WAL must acquire this lock. Under low concurrency, the lock is held for microseconds. Under high concurrency, contention becomes the dominant factor.

When 500 concurrent sessions are all trying to write to the WAL simultaneously, the WAL insert lock becomes a hot spot. Each transaction holds the lock for the duration of the WAL write, which includes copying the record into the WAL buffer and potentially flushing the buffer to disk if the buffer is full. The lock is released only after the write is complete. In a system with 4,000 WAL writes per second, the lock is held and released thousands of times per second. The spinlock overhead alone can consume 30-40% of a CPU core.

But the real killer is the interaction between the WAL insert lock and the session table row lock. Each session update requires a row-level lock on the session table. The row lock is held until the transaction commits. The transaction cannot commit until the WAL write is complete. The WAL write cannot complete until the WAL insert lock is acquired. This creates a chain of dependencies that can deadlock the system under high concurrency.

In the March 2024 test, the system entered a state where 487 sessions were all waiting on the WAL insert lock. Each session had a row lock on its own session row. No session could release its row lock until its WAL write completed. No WAL write could complete until the WAL insert lock was freed. But the WAL insert lock could not be freed because the archiver was backlogged and the WAL buffer was full. The system was effectively deadlocked. The only way out was for the database to kill transactions, which it did after a 30-second timeout. The result was a cascade of failed spins, lost session state, and angry players.

This is not a bug in PostgreSQL. It is a feature of the WAL architecture that becomes a liability under the specific write patterns of slot sessions. The WAL is designed for general-purpose transactional workloads, not for a system that generates thousands of small, concurrent writes per second. PostgreSQL’s concurrency model assumes that most transactions are short-lived and that contention for the WAL insert lock is rare. In iGaming, every transaction is short-lived, but they all arrive at the same time, creating a perfect storm of lock contention.

Mitigations That Don’t Work and One That Does

The obvious fix is to increase max_wal_size and wal_buffers. This buys time, but it does not solve the underlying problem. Larger WAL buffers delay the flush, but they also increase the amount of data lost in a crash. Larger max_wal_size means the archiver has more segments to process, which can actually worsen the backlog. In the March 2024 test, increasing max_wal_size from 64 GB to 128 GB delayed the stall by about 40 minutes, but when the stall hit, it was worse, because the WAL had accumulated 120 GB of unarchived segments. Recovery took 22 minutes instead of 14 seconds.

Another common mitigation is to batch session updates. Instead of updating the session row on every spin, the game server accumulates spin data in memory and writes it to the database in bulk every 5 or 10 seconds. This reduces WAL writes by a factor of 5 to 10. But it introduces a new problem: the session state in the database is always behind the real-time state in the game server. If the game server crashes, the last few seconds of spins are lost. In regulated markets, this violates the requirement for a complete, auditable record of every spin. The platform cannot claim to have a “single source of truth” if the database is deliberately kept stale.

The mitigation that actually works is to separate the session tracking from the core ledger. Instead of storing session state in the same PostgreSQL instance that handles transactions, the session data is written to a separate, high-write-throughput database. Redis is the most common choice. The session state lives in Redis, with periodic snapshots to PostgreSQL for persistence. The core ledger only records the final outcome of each session: the total bets, total wins, and net result. This reduces the WAL write rate by an order of magnitude, because the ledger only sees one write per session instead of hundreds.

This architecture is not new. It is standard practice in high-frequency trading and real-time bidding systems. But many iGaming platforms, especially those built on monolithic Django or Ruby on Rails stacks, resist the split because it complicates the transaction model. The session data in Redis must be reconciled with the ledger data in PostgreSQL during session closure, and any inconsistency can lead to balance disputes. In the March 2024 test, the platform that successfully handled 500 concurrent sessions used a dual-database architecture with a custom reconciliation layer. The PostgreSQL instance never saw more than 50 WAL writes per second, even during peak load.

The Unspoken Cost of Session-Per-Spin

The 500-session WAL stall is a symptom of a deeper architectural choice: the decision to treat every spin as an independent database transaction. This is the “session-per-spin” model. It is the default in many iGaming platforms because it is simple to implement and easy to audit. Every spin is a row in a spins table. Every spin is a transaction. Every transaction writes to the WAL. The database becomes a log of every event, which is great for compliance but terrible for performance.

There is an alternative: session-based accounting. Instead of writing a row for each spin, the platform writes a row for each session, with a running total of bets and wins. The spin-level detail is stored in a separate, less critical store, such as a time-series database or a log file. The PostgreSQL instance only sees the session-level writes. This reduces the WAL write rate by a factor of 100 or more. But it requires a fundamental shift in how the platform thinks about data integrity. The spins table is no longer the source of truth for individual spin results. The source of truth becomes the session table, with the spin logs serving as an audit trail.

Regulators in the United States have not yet mandated a specific architecture for spin record-keeping. The New Jersey Division of Gaming Enforcement requires that “all game play shall be recorded in a manner that allows for reconstruction of play,” but it does not specify whether the reconstruction must be at the spin level or the session level. The Michigan Gaming Control Board has similar language. In practice, most platforms err on the side of recording every spin, because it is the safest interpretation of the regulation. But the WAL stall is the cost of that safety.

The open question is whether the industry will continue to pay that cost as concurrent session counts grow. The 500-session threshold is not a hard ceiling. It is a function of hardware, configuration, and architecture. With faster storage, more memory, and careful tuning, a PostgreSQL instance can handle 1,000 concurrent sessions. But the curve is exponential. The WAL write rate scales linearly with the number of sessions, but the lock contention scales quadratically, because every additional session increases the probability of a lock conflict. At some point, the database will stall, and the stall will be catastrophic.

The next generation of iGaming platforms will need to decide whether PostgreSQL is the right database for the session table at all. Or whether the WAL stall is simply the cost of doing business, and the solution is to throw more hardware at it until the next threshold is hit. The answer depends on whether the industry wants to build for 500 sessions or 5,000. And that decision will determine whether the 3 a.m. crashes become a relic of the past or a recurring nightmare.