Bali, Indonesia
Insights

When Page Cache Chokes the Ledger: Debugging Kernel Dirty Writeback Stalls in Core Banking Nodes

October 14, 2026
0:00
During peak mobile transaction windows—such as salary disbursement cycles and holiday shopping surges encountered across expanding retail networks like CCB Indonesia—the infrastructure supporting digital banking faces an unforgiving test. Mobile apps initiate thousands of simultaneous fund transfers, bill payments, and balance inquiries. In a compliant financial backend, none of these actions are mere transient memory mutations; every step emits immutable audit records, transaction event journals, and database write-ahead log (WAL) entries that must reach persistent storage before an acknowledgement returns to the client. Everything looked healthy on our host dashboards during a major load spike: CPU utilization sat comfortably below 45%, network interfaces showed plenty of headroom, and average disk latency on our NVMe-backed SAN was reporting single-digit milliseconds. Yet, downstream API gateways began throwing 504 Gateway Timeouts. Application worker threads on core database nodes were stalling by the hundreds, stuck in uninterruptible sleep (D-state). The culprits were neither deadlocks in the relational database engine nor physical drive degradation. Instead, we were hitting a classic Linux memory management trap: the kernel's dirty page writeback throttling mechanism.
To understand why a system with hundreds of gigabytes of RAM suddenly grinds to a halt under write pressure, we have to examine how the Linux virtual memory manager (VMM) handles buffered writes. By default, when an application invokes write(), the kernel copies data from user space into the page cache, marks those pages as dirty, and immediately returns execution to the application. This makes writes appear instantaneous. In the background, kernel flusher threads (kworker pool running wb_workfn) periodically scan the dirty list and flush those pages to the underlying storage block device. Two critical kernel tunables govern this behavior: vm.dirty_background_ratio and vm.dirty_ratio. By default on many distributions, these are configured to 10% and 20% (or 30%) of total available system memory, respectively. On a legacy server with 4GB of RAM, a 10% dirty threshold amounts to roughly 400MB—a buffer easily ingested by modern flash storage without perceptible latency spikes. However, on modern hosted enterprise bare-metal hypervisors and large database guest VMs provisioned with 256GB or 512GB of RAM, these percentages become liabilities. At 256GB of RAM, a 10% background threshold translates to 25.6GB of unwritten data, while a 20% ceiling represents over 51GB.
BASH
# Default kernel ratios on large memory nodes introduce massive write queues
sysctl -w vm.dirty_background_ratio=10
sysctl -w vm.dirty_ratio=20
When a rapid transaction wave hits, mobile banking services flood the audit log collector and transactional queues with writes faster than the storage layer can retire them. The volume of dirty memory quickly blows past vm.dirty_background_ratio. The kernel wakes up background flushers, but before they can make a dent in the tens of gigabytes of accumulated pages, dirty memory breaches vm.dirty_ratio. At this threshold, the kernel triggers synchronous throttling via balance_dirty_pages(). The calling thread is forcibly stripped of its asynchronous privilege: any process attempting another buffered write is forced to pause and synchronously flush dirty pages to disk itself. Because every database connection thread writing ledger events or WAL records suddenly gets hijacked to perform heavy page cleaning, the application's request pipeline collapses simultaneously.
This behavior is exacerbated on hosted virtualized infrastructure. In cloud environments or private hypervisors running KVM/QEMU, I/O passes through layers of guest kernel page caches, virtio queues, and host storage controllers. When a guest kernel suddenly forces all active worker threads into synchronous writeback, it submits massive bursts of I/O requests to the virtual block device driver (virtio-blk or virtio-scsi). The virtual queue depths quickly saturate. Backpressure ripples through the guest kernel's I/O scheduler directly into balance_dirty_pages_ratelimited(). Worse still, if the host hypervisor is also relying on buffered I/O on its physical backing store, a phenomenon known as double caching occurs. The guest flushes dirty pages into what it considers raw storage, but the host intercepts them into its own page cache, triggering a secondary dirty writeback throttle on the physical bare-metal host. The resulting latency variance turns sub-millisecond writes into multi-second disk stalls, starving transactional heartbeat checks and triggering spurious cluster node evictions.
Eliminating writeback stalls requires shifting the kernel from ratio-based limits to strict, low-capacity byte limits. On high-memory database and ledger nodes, dirty page thresholds should never be calculated as percentages of memory. Instead, set vm.dirty_background_bytes and vm.dirty_bytes to values that the underlying storage controller can flush in under 200 milliseconds.
BASH
# Enforce strict byte limits to keep page cache flushes predictable
sysctl -w vm.dirty_background_bytes=67108864   # 64 MB
sysctl -w vm.dirty_bytes=268435456             # 256 MB
By capping the background writeback initiation at 64MB and the hard blocking threshold at 256MB, the kernel initiates continuous, trickle flushes to disk almost immediately upon dirty memory arrival. The flusher threads keep the I/O queue shallow, completely preventing the runaway accumulation of 50GB of dirty pages that leads to synchronous thread locking. Architecturally, core banking platforms must physically and logically segregate write patterns. Relational WAL and audit journals should be mounted on dedicated physical volumes bypass-cached using direct I/O (O_DIRECT or O_DSYNC), ensuring sequential writes never compete with the shared Linux page cache. Furthermore, deploying Linux control groups (cgroups v2) with strict io.weight and io.max policies allows systems engineers to throttle auxiliary audit log shipping agents, preventing non-critical logging pipelines from starving the writeback bandwidth required by the primary transactional ledger engine. Diagnosing these stalls in real time requires looking beyond standard CPU and disk averages. Tools like vmstat 1 reveal runaway numbers in the b (blocked processes) column alongside surging bo (blocks out) activity. Using eBPF scripts such as biosnoop and tracing kernel tracepoints like writeback:balance_dirty_pages provides unassailable visibility into exactly how many nanoseconds database worker threads spend stalled in kernel-enforced write pauses. Tuning the page cache guarantees that high-throughput mobile banking nodes maintain predictable latency, keeping financial ledgers durable without sacrificing system stability.