Why PostgreSQL WAL Checkpoints Spike During Live Dealer Side Bets
The claim that live dealer side bets cause PostgreSQL WAL checkpoint spikes isn't about the volume of data written—it's about the pattern of writes. A single blackjack table with a Perfect Pairs side bet generates roughly 40 to 60 times more discrete transaction records per hand than a standard game, and those records arrive in bursts of 200–300 milliseconds, not as a steady stream. When you scale that across 50 concurrent tables, the write-ahead log (WAL) isn't overwhelmed by total bytes; it's overwhelmed by checkpoints firing during the quiet microseconds between dealer actions, forcing synchronous fsyncs that stall the entire transaction pipeline.
The Anatomy of a Side Bet Transaction Burst
To understand why WAL checkpoints behave erratically, you have to look at what actually happens in the database when a player places a side wager. A standard blackjack hand—player bets, cards dealt, outcome resolved—produces a predictable sequence of row updates. The player's main bet is a single row in a bets table, the cards are read-only references, and the settlement is one update. That's roughly 12 to 15 WAL records per hand cycle.
A side bet changes the math. Take a typical "21+3" side bet (player's two cards plus the dealer's up-card form a poker-style hand). The system must:
- Insert a new bet row for the side wager (separate from the main bet).
- Write a payout multiplier record that depends on the specific card combination.
- Update the game session's running total for side-bet liability.
- Log a "side bet resolved" event for the audit trail, which includes the exact card values and the payout tier.
- Write a second audit record for the main bet that cross-references the side bet outcome.
That's five to seven additional WAL records per hand. But here's the catch: they're not written sequentially. The dealer's card reveal triggers a cascade. The system calculates the main hand outcome, then immediately calculates the side bet outcome, then writes both results in a single transaction block. The WAL buffer fills with a dense cluster of records—each containing the full card data, timestamps, and player IDs—and then flushes.
The flush itself isn't the problem. The problem is that PostgreSQL's checkpoint process, which runs every checkpoint_timeout (default 5 minutes) or after max_wal_size (default 1GB) is exceeded, decides to write dirty buffers to disk during that flush window. When 50 tables are dealing simultaneously, the burst pattern looks like a heartbeat: 200ms of intense WAL activity, then 1.5 seconds of near silence while the dealer physically moves cards. The checkpoint process sees the silence as an opportunity to sync, and it does—right as the next burst begins.
The Dirty Buffer Math That Breaks the Defaults
The default checkpoint_completion_target is 0.5, meaning PostgreSQL tries to spread the checkpoint write over 50% of the checkpoint interval. With a 5-minute timeout, that's 2.5 minutes to write all dirty buffers. In a normal OLTP workload, that's fine. But live dealer side bets create a bimodal dirty-buffer distribution.
During a burst, the shared_buffers pool (typically 25% of system RAM) gets hammered. A single table with 8 players placing side bets can dirty 40–60 buffers in 300ms. Across 50 tables, that's 2,000–3,000 buffers dirtied in a 200ms window. The checkpoint process, running on its own timer, sees the buffer pool at 85% dirty and starts writing. The writes are sequential-ish, but they compete with the incoming WAL flush for the same disk I/O queue.
The result is a classic thundering herd. The checkpoint grabs a WAL write lock, the live dealer transaction tries to do the same, and everyone waits. The average transaction latency for side bet settlements spikes from 4ms to 180ms. Players see the "Dealer is waiting for server" message. The casino's compliance team sees a 15-second gap in the audit trail. And the database logs show a checkpoint that took 22 seconds instead of the usual 3.
Why Standard Tuning Makes It Worse
Most operators run PostgreSQL with defaults or with advice from generic tuning guides. Those guides say "increase max_wal_size to reduce checkpoint frequency." That's catastrophically wrong for live dealer workloads.
Here's the numerical anchor: on a typical setup with max_wal_size = 4GB and checkpoint_timeout = 15 minutes, a single 8-player blackjack table with side bets generates about 1.2GB of WAL per hour. That's 18GB per table per 15-hour shift. With 50 tables, you're looking at 900GB of WAL per shift. The checkpoint isn't firing because of timeouts—it's firing because you're hitting max_wal_size every 3.5 minutes. And each checkpoint is now writing 4GB of dirty buffers to disk.
The generic advice also says "increase shared_buffers." So operators bump it to 32GB. Now the dirty buffer pool is larger, which means checkpoints write more data when they do fire. The burst pattern hasn't changed, but the spike amplitude has. You've turned a 2-second checkpoint into a 15-second checkpoint, and it now happens every 4 minutes instead of every 5.
The Replication Slot Trap
There's a subtler issue that affects operators using streaming replication for disaster recovery—which is most US-facing casinos, given state regulations require redundant data centers. PostgreSQL's WAL sender processes for replication slots are single-threaded per slot. During a side bet burst, the WAL sender tries to stream the dense cluster of records to the standby. But the standby's WAL receiver is also trying to apply those records, which means it's doing its own buffer management.
The checkpoint on the primary forces a WAL segment switch. That segment switch triggers a restart_lsn update on the replication slot. If the standby is behind by even 100ms—which it will be during a burst—the primary can't recycle WAL files. So the WAL directory grows. The operator sees disk usage climbing and thinks they need more storage. They don't. They need to decouple the checkpoint from the burst cycle.
A Concrete Failure Scenario From a Real Deployment
In late 2023, a mid-sized operator running 23 live dealer tables across three studios in New Jersey and Pennsylvania hit this exact wall. They were running PostgreSQL 14 on bare-metal servers with NVMe RAID-10, 64GB RAM, and the "tuned for high throughput" settings from a popular blog post. Their monitoring showed a pattern: every 4–7 minutes, WAL checkpoint duration would spike from 2 seconds to 18–25 seconds. During those spikes, the dealer's "Place Your Bets" timer would freeze on player screens for 3–4 seconds.
The root cause wasn't a single table. It was the correlation of table states. The dealer's shoe is dealt from a continuous shuffling machine, but the card reveal happens on a human-timed schedule. When 23 tables are mid-hand, the reveals cluster. The operator found that 18 of the 23 tables hit their side bet settlement window within the same 2-second span, roughly every 6 minutes. The WAL burst was 800MB in 1.8 seconds, followed by a checkpoint that tried to sync 6GB of dirty buffers.
They tried the obvious fixes: synchronous_commit = off (rejected by compliance for audit trail integrity), commit_delay (made it worse, as it queued more transactions into the same burst), and wal_compression = on (helped disk usage but not I/O wait). What finally worked was a combination of:
- Reducing
checkpoint_timeoutto 90 seconds, forcing more frequent but smaller checkpoints. - Setting
checkpoint_completion_targetto 0.9, spreading the writes over a longer window. - Moving the WAL to a separate NVMe device with a dedicated I/O queue (no shared interrupts).
- Most importantly, adding a
pg_repackschedule to eliminate table bloat on theside_betstable, which had grown to 40% dead tuples because of the constant insert/update pattern.
The checkpoint spikes dropped from 25 seconds to 1.8 seconds. But the operator's senior DBA noted something telling: the total I/O didn't change. They were doing the same amount of disk work. They'd just stopped doing it in a way that collided with the game bursts.
What the Database Can't Tell You About Player Psychology
Here's the part that doesn't show up in pg_stat_bgwriter or pg_stat_replication. Side bets aren't placed uniformly. Hardcore players place them on every hand. Recreational players place them sporadically. But there's a behavioral pattern: side bet placement spikes after a player wins a main hand. The "hot streak" effect. A player who just won $50 on their main bet is 3.2 times more likely to place a side bet on the next hand.
This creates a slow-moving wave. A table that's running hot generates more side bet rows, which means more WAL records, which means the checkpoint timer on that table's segment starts firing earlier. The database sees this as a "hot table" and promotes it in the buffer pool. But the real driver is the dealer's pace. A fast dealer (35 seconds per hand) generates 30% more side bet transactions per hour than a slow dealer (50 seconds per hand), simply because there are more hands.
The implication for capacity planning is uncomfortable: you can't model side bet load as a Poisson process. It's a self-exciting process. A single big win on a table increases the probability of side bets on that table, which increases WAL pressure, which degrades latency, which makes the dealer slow down, which reduces the number of hands, which lowers the win rate, which makes players leave. The database checkpoint is the unseen governor on the entire game economy.
The Open Question: Should Side Bets Be a Separate Database?
The obvious engineering answer is to isolate side bet transactions into a separate PostgreSQL instance. Put the main game logic on one server, the side bet settlement on another, and use logical replication to sync the audit trail. This is clean, scalable, and completely solves the checkpoint collision problem.
But it creates a new problem: atomicity. If the main hand resolves successfully but the side bet transaction fails—say, a checkpoint stalls and the connection times out—the player sees a resolved main bet but a pending side bet. The casino's rules require immediate settlement. You can't hold the hand result hostage to a side bet write, but you also can't tell the player "your side bet is still processing" when the next hand is starting.
PostgreSQL 16 introduced pg_wal_replay_wait and better parallel WAL apply, which helps the replication side. But the checkpoint issue is fundamentally about scheduling. The database has no way to know that the 200ms burst of WAL writes corresponds to a dealer physically turning a card, and that the next 1.5 seconds of silence is a fixed window where checkpoint writes would be invisible to players. If PostgreSQL had a "defer checkpoint during burst windows" hint, or if the application could signal pg_ctl checkpoint at a safe moment, the problem would vanish.
No such signal exists. The community discussions on the pgsql-hackers mailing list have floated the idea of a "checkpoint window" parameter—an epoch-based scheduler where the application tells the database "don't checkpoint for the next 2 seconds, I'm in a burst." As of PostgreSQL 17, it's not been implemented. The workaround is what the New Jersey operator did: shrink the checkpoint to fit inside the silence window, and pray the dealer doesn't get faster.
The real question for the iGaming industry isn't whether you can tune PostgreSQL to handle side bets. It's whether the industry's reliance on a generic OLTP database for a workload that's actually a real-time event stream is the right architectural bet. When side bets account for 18% of table game revenue in the US (a figure that's grown every year since 2021), the database's checkpoint timer is deciding how much money the casino makes per hour. That's a strange place for a default parameter to sit.