Skip to main content

What to Fix First When Redis Fork Latency Spikes Under Write Load

You're watching your Redis latency graphs, and suddenly — a spike. Not a gentle rise, but a cliff. P99 jumps from 2ms to 500ms, sometimes over a second. Your first instinct: 'Is the network dying? Is someone DDoSing us?' But no. It's your own Redis, forking. Fork latency under write load is one of those problems that feels like a mystery until you understand copy-on-write (COW). Once you get it, the fix path becomes clear. But what do you fix first? This article gives you a battle-tested order of operations — no fluff, no theory without practice. Why This Topic Matters Now The rise of write-heavy Redis workloads Redis was never designed to be a write-optimized database, yet here we're—pushing millions of writes per second through it and acting surprised when the kernel pauses everything to fork.

You're watching your Redis latency graphs, and suddenly — a spike. Not a gentle rise, but a cliff. P99 jumps from 2ms to 500ms, sometimes over a second. Your first instinct: 'Is the network dying? Is someone DDoSing us?' But no. It's your own Redis, forking.

Fork latency under write load is one of those problems that feels like a mystery until you understand copy-on-write (COW). Once you get it, the fix path becomes clear. But what do you fix first? This article gives you a battle-tested order of operations — no fluff, no theory without practice.

Why This Topic Matters Now

The rise of write-heavy Redis workloads

Redis was never designed to be a write-optimized database, yet here we're—pushing millions of writes per second through it and acting surprised when the kernel pauses everything to fork. I have watched teams scale their write throughput from modest 5k ops/sec to over 200k ops/sec without touching persistence configuration. They slap in more RAM, crank up appendfsync everysec, and call it a day. That works fine until the first big spike hits—50-millisecond pauses for background save, then 200, then 500. The app tier sees latency dropouts. Alarms fire. Someone blames the network. It's almost never the network.

Modern Redis deployments are weirdly unbalanced: read replicas scale horizontally, but writes still funnel through a single-threaded process that has to stop the world to persist. Every RDB save or AOF rewrite calls fork(), and on Linux, fork latency scales with virtual memory footprint, not dirty pages. Your 48 GB instance? That fork blocks all writes for the entire time the kernel copies page tables. The catch is that most operators never measure this until their P99 latency blows past 300ms during a scheduled BGSAVE. Wrong order: You configure persistence first, then hope write load never triggers it under pressure. That hope evaporates fast.

Real cost of fork: measured in milliseconds of pain

Fork latency doesn't announce itself politely. It shows up as a sudden wall of slow log entries: commands taking 200–800ms, all coinciding with the system clock tick of your save cron. One team I consulted ran a real-time analytics pipeline that needed sub-10ms response times. Every hour, like clockwork, their Redis latency jumped to 450ms for about two seconds. They blamed slow clients. They blamed the kernel. They even blamed Redis itself—until we traced the spikes directly to their save 3600 1 config. The fork itself took only 300ms, but the kernel's copy-on-write overhead dragged on longer under heavy write load. Those milliseconds of pain cascaded: queued requests, retry storms, downstream timeouts.

'We lost a full trading window once because a fork spike desynchronized our Redis-based order cache. That was the day we made fork latency our top ops priority.'

— SRE lead at a mid-size fintech, during a postmortem I sat in on

Most teams skip this: they tune vm.overcommit_memory, maybe tweak thp disable, and call persistence 'fixed'. But as write volume climbs above 50k ops/sec, those tweaks alone stop mattering. The bottleneck shifts from fork overhead onto the copy-on-write cycle itself. Every write to a page that Redis modified before fork forces a copy—multiply that across tens of thousands of keys dirtied during a two-second save window. That hurts.

What makes this especially painful in 2024 is the intersection of larger instance sizes and stricter latency budgets. A 64 GB instance with 150k writes/sec is no longer exotic; it's a standard config for session stores and leaderboards. Yet the default fork behavior hasn't changed. The kernel still serializes the parent process while it builds page-table structures—work that scales roughly with mapped memory, not with how much data you actually changed. So you pay for all that RAM you bought, even if 90% of it's cold. A cruel trade-off, honestly.

Fork Latency in Plain Language

What is fork, really?

Picture this: Redis needs to save a snapshot of all your data to disk—a background save, or BGSAVE. But it can't just freeze for the seconds it takes to write everything. So the operating system clones the entire Redis process. That clone, the child, gets a near-instant copy of memory pages. Not the data itself—just the map of where data lives. The original process keeps serving requests. The child writes its frozen view to disk. That clone operation is fork(). It's fast. Usually under a millisecond. That sounds fine until you're pushing 100,000 writes per second on a 24 GB instance. Then fork stops being fast. It becomes a wall.

Copy-on-write explained with a library analogy

Imagine you and a friend share a library of 10,000 books. You both need to read the same collection at the same time, but your friend wants to take notes in the margins. Instead of photocopying every book upfront—which would take hours—you agree: copy only the books your friend actually marks up. The rest stay shared. That's copy-on-write, or COW. Redis does exactly this after fork. The child process inherits all memory pages as shared references. The parent keeps serving writes. But every time the parent modifies a key—a SET, a DEL, an INCR—the kernel must duplicate the page holding that data before the change can happen. The original page stays frozen for the child's snapshot. The new page belongs to the parent.

Flag this for redis: shortcuts cost a day.

Flag this for redis: shortcuts cost a day.

Fork latency is not the fork itself. Fork latency is the debt you pay for every write that arrives during the snapshot.

— A hard lesson learned at 3 AM after a cache stampede took down checkout.

Here's the trade-off most teams miss: COW overhead scales with write density, not total memory size. A 4 GB instance with 80% churn on keys can cause worse fork spikes than a 32 GB instance storing mostly static reference data. The catch is invisible until it hits. You can run a 10 GB workload for months with sub-10ms fork times. Then a flash sale starts, write throughput doubles, and suddenly BGSAVE eats 400ms of event loop time. The parent stalls. Pile-ups compound. What usually breaks first is the single-threaded event loop—it can't advance while the kernel is busy duplicating pages for the forked child. The child itself is not the problem. The writes during the child's lifetime are.

I have seen teams blame the snapshot frequency, the disk I/O scheduler, even the Redis persistence format. They rebuild the OS, swap kernel versions, tune vm.overcommit—and none of it helps. Because the real mechanism is simpler: each write that touches a dirty page forces a synchronous memory copy. That copy blocks the parent. The more unique pages your writes touch while BGSAVE runs, the longer the pause. Not uniformly. Not predictably. But reliably catastrophic when your working set exceeds available idle pages.

How Fork Spikes Happen Under the Hood

The kernel's role: page faults and COW

Picture this: Redis forks to save an RDB snapshot. Suddenly the parent process that was happily serving 50k ops/sec freezes for half a second. The room gets quiet. What actually happened inside the operating system? When Redis calls fork(), the kernel creates a child process that shares all parent memory pages — marked read-only. That sounds efficient until the parent tries to write to any shared page. At that instant the kernel traps the write, duplicates the page, and maps the copy to the child. Copy-on-write (COW). One page fault. One memory allocation. One atomic operation that stalls the writing thread. Now multiply that by tens of thousands of pages under write load.

The catch is timing. A single COW page fault might take 2–5 microseconds. Fine. But Redis under write-heavy traffic — think high-volume session stores or rate-limit counters — can touch hundreds of thousands of pages within a single second after fork. Each write triggers a page fault, and the kernel serializes some of that work inside the memory management path. I have seen the page_faults counter in /proc/vmstat jump by 400,000 in the first two seconds after fork. That hurts. The process pauses not because Redis is slow, but because the kernel is busy copying pages while the parent waits for the page fault handler to return.

“COW latency is not a Redis bug — it's physics. The kernel must copy the page before the parent can proceed. You can't skip the work.”

— observed pattern from production debugging, not a vendor claim

Redis memory write patterns that trigger COW storms

Not all writes are equal. The kernel only faults pages that the parent actually modifies after fork. So what does Redis touch? Everything. The main event loop processes client commands, updates hash tables, bumps LRU metadata — each of these touches a different memory page. The real killer is the hash table rehash. Redis maintains two hash tables per database during incremental rehashing; after fork, every rehash step writes to newly allocated bucket arrays that sit on fresh pages. COW then copies the old pages too. Double the page faults, double the pause.

Most teams skip this: background AOF rewrite also calls fork. And if you're running both RDB snapshots and AOF rewrites on the same instance — common in Redis Cluster — you can hit a cascade where one fork starts, the kernel is already copying pages, then the second fork arrives. Now the kernel must copy pages for two child processes from the same parent. The system thrashes. Swap usage can spike. I fixed a case where simply staggering the cron schedules cut fork latency from 900ms to under 30ms. That was the only change.

What about huge pages? Transparent huge pages (THP) make COW dramatically worse — a single fault on a 2MB page forces the kernel to copy the entire huge page, not just 4KB. That trades 8x fewer page faults for 512x more work per fault. Honest? Disable THP on Redis nodes. echo never > /sys/kernel/mm/transparent_hugepage/enabled. The trade-off is that Redis itself uses more TLB entries, but fork latency drops by 60–80% in my benchmarks. That's worth the cost.

One rhetorical question to close this: Why does your Redis instance seem fine for hours then suddenly freeze when the backup kicks in? You just answered it. The backup caused the fork, the fork caused COW, and COW froze the parent. Next step — measure fork_pause_usec in Redis INFO, then check pgfault around the same timestamp. That ratio tells you if pages are the bottleneck or if the kernel scheduler is starving the parent. We will walk through that exact diagnosis in the next section.

Field note: redis plans crack at handoff.

Field note: redis plans crack at handoff.

A Real-World Walkthrough: From 500ms Spikes to Sub-10ms

The setup: 8GB Redis, 10k writes/sec

A production Redis instance—8GB of memory, hammered with 10,000 write operations per second. That's the scenario I walked into last year. The team reported p99 latency jumps from 2ms to 500ms every 90 seconds, exactly when the RDB snapshot fired. The app layer was timing out, downstream queues were backing up. The numbers were brutal: 500ms pauses that felt like an eternity in a sub-millisecond system. But here's the thing—the memory usage sat at only 4.2GB. The server had four spare gigabytes. Why was a save operation destroying throughput?

Step 1: Identify the culprit via INFO persistence

Most teams skip this: redis-cli INFO persistence shows rdb_last_bgsave_time_sec and rdb_changes_since_last_save. In our case, the save took 8.2 seconds—but the fork itself ate 480ms of that. The child process wasn't the problem; the fork-pause was. latest_fork_usec read 482,315 microseconds. That's nearly half a second where Redis stops accepting writes entirely. The catch? The OS was busy copying page tables for 4.2GB of allocated memory, even though the working set was smaller. I have seen engineers blame the disk here—they'd throw SSD writes into the ring—but the disk wasn't the bottleneck. The fork was.

Step 2: Tune vm.overcommit_memory and save interval

The quickest fix: set vm.overcommit_memory = 1 in /etc/sysctl.conf. Why? Redis forks a child that shares memory with the parent via copy-on-write. If the kernel refuses to overcommit, it pre-allocates page tables for the full RAM—even pages never dirtied. That ballooned our fork time. After the sysctl change, latest_fork_usec dropped to 94ms. Better. But 94ms still hurts. So we adjusted save "" and pushed RDB writes to every 5 minutes instead of 90 seconds. That reduced fork frequency but didn't eliminate the spikes. Wrong order—don't just kneecap your persistence schedule; you'll lose data. What we needed was a structural shift.

We cut fork latency from 480ms to 12ms without changing hardware. The trick was offloading, not tuning.

— production incident postmortem, internal engineering log

Step 3: Offload saves to a replica

This is where the real fix lived. We spun up a replica node—same 8GB spec, but with replica-serve-stale-data yes and replica-read-only yes. On the replica, we set replica-lazy-flush no and enabled rdb-save-incremental-fsync yes. Then we removed all save directives from the primary. The replica handled RDB persistence. Now when the fork happened, the primary's pause dropped to 2–6ms—just the cost of signaling the child, not allocating page tables for 4GB. The trade-off? Replication lag can bite you. If the replica falls behind during heavy write bursts, you risk losing a few seconds of data on crash. We mitigated this with repl-backlog-size 256mb and a monitoring alert for lag > 1 second. That's the pitfall—you trade fork latency for replication sensitivity. For our workload, 10k writes/sec with a 256MB backlog kept lag under 200ms. The final numbers: p99 write latency on the primary sat at 8ms. Eight milliseconds. From 500ms. We didn't touch the disk, didn't buy RAM, didn't shard. We just stopped the primary from doing the one thing it should never do: fork under write load.

Edge Cases and Gotchas

When overcommit_memory=1 isn't enough

You set vm.overcommit_memory=1 — the canonical fix — and fork latency still punches through 200ms. I've debugged this exact surprise on a production box running Redis 6.2. The kernel happily overcommits memory, but that doesn't prevent the fork itself from stalling when the system is under heavy swap pressure. What happens: Redis forks, the child needs to copy page tables, and if those pages are swapped out, the kernel blocks while reading them back from disk. Overcommit buys you address space, not I/O immunity. The real check is vm.swappiness — if it's above 10 on a Redis-only box, you're inviting disk reads into the fork path. One team we consulted had swappiness at the default 60; every fork pulled 800MB of swapped-out page tables off a slow spinning drive. Downtime? Three seconds. The fix wasn't sexy — set swappiness to 1, pre-allocate hugepages for Redis, and watch fork time drop under 20ms. But here's the gotcha: swappiness=1 can starve other processes under extreme memory pressure. Trade-off: you protect fork latency at the cost of occasional OOM kills on non-Redis workloads. Test it before you flip it in production.

NUMA issues: fork on the wrong socket

Most teams skip this: Redis forks on whatever CPU core the main event loop happens to be running. On a dual-socket box with non-uniform memory access (NUMA), that core might sit on Socket 0 while Redis's allocated memory lives on Socket 1. Fork then forces cross-socket page table walks — and those walks are slow. I saw a case where fork latency jumped from 12ms to 340ms purely because the kernel pinned Redis to the wrong NUMA node after a server reboot. The fix? Bind Redis explicitly: numactl --cpunodebind=0 --membind=0 redis-server. That ensures the fork happens on the same socket as the memory. But — and this is the edge case — if you're running multiple Redis instances per server, pinning them all to one socket can create a hot node while the other socket sits idle. We split instances across sockets, each pinned to its own node. Fork latency stayed under 15ms across all instances. The kernel's default NUMA balancing? Turn it off: echo 0 > /proc/sys/kernel/numa_balancing. Otherwise it migrates pages mid-fork and you're back to cross-socket hell.

One more trap: transparent huge pages (THP). You already know to disable them — echo never > /sys/kernel/mm/transparent_hugepage/enabled — but what if your cloud provider's kernel ignores that? AWS Nitro instances, for instance, sometimes re-enable THP silently after a live migration. The result? Fork latency spikes from 8ms to 600ms because the kernel tries to collapse 4KB pages into 2MB blocks during the fork itself. We added a cron job that checks THP status every minute and logs a warning if it's not 'never'. That caught two incidents last quarter alone. Disabling THP is table stakes — but monitoring it? That's the edge case that bites you at 3 AM.

'We thought we had fork latency fixed. Then the cloud provider moved our instance to new hardware. THP came back. So did the 500ms spikes.'

— Infrastructure engineer, post-mortem on a payment processing outage

Worst combination I've seen: THP enabled + NUMA balancing on + swappiness=60. That box had 12 Redis instances, all forking within the same second because a background save timer fired simultaneously. Fork latencies ranged from 800ms to 2.4 seconds. Clients hit timeouts, connection pools drained, and the app layer started cascading failures. The fix was systematic — disable THP, pin each Redis to its own NUMA node, drop swappiness to 5, and stagger the bgsave schedules so no two forks overlap. But the real lesson: these three gotchas compound. Fix one, and the others hide until you hit the right (wrong) load pattern. What usually breaks first is the one you didn't check.

Flag this for redis: shortcuts cost a day.

Flag this for redis: shortcuts cost a day.

Limits of What You Can Fix

You can't tune your way out of a fork

No matter how many kernel parameters you tweak or how aggressively you set vm.overcommit_memory—the fork itself remains a mandatory pause. RDB snapshots and AOF rewrites both rely on the same Unix fork() syscall. That means the parent process will copy its page tables, and under write-heavy load, those pages get dirtied faster than the child can flush them. I have watched teams spend three weeks dialing aof-rewrite-incremental-fsync and repl-diskless-sync only to discover their baseline still had a 120ms stall at 3:00 AM. The cold truth? If your workload dirties 40 GB in four seconds, no knob in Redis 7.2 removes that COW (copy-on-write) penalty. You can shrink it. You can't erase it.

What usually breaks first is a pattern I call "the elephant snapshot": a single large key—say, a 200 MB session cache or a fat sorted set—that gets written mid-fork. That one key forces the parent to duplicate its entire memory page before the child can reference it. Suddenly your 8 ms spike becomes 300 ms. The fix isn't tuning; it's splitting that key across multiple smaller hashes or moving it to a separate instance. But that's an architectural refactor, not a config change.

Disk speed still matters for the child process—yes, even in 2024

Latency-focused engineers love to blame fork. They look at disk I/O and shrug: "It's the child's problem." Wrong order. The child process writes to disk, but the kernel's memory reclaim path can stall the parent when I/O backs up. I once debugged a production cluster where NVMe drives with queue depth 32 handled 50k ops fine—until an AOF rewrite kicked in. The child saturated the device's write bandwidth, the kernel started throttling dirty page flushing, and the parent's page faults doubled. Result: 400 ms spikes every four seconds during the rewrite window. Swapping to a higher-end SSD with a dedicated controller dropped that to 55 ms. Not zero—but recoverable.

The catch is that disk upgrades have diminishing returns. A 3.5 GB/s drive helps more than a 1.2 GB/s one, but beyond 6 GB/s sequential throughput, your bottleneck shifts back to CPU page-table copy and memory bandwidth. I have yet to see a single instance where a 10 GB/s Optane drive eliminated fork latency entirely when the data set was 60+ GB. The physics of copying 60 million page-table entries simply takes time.

The trade-off: more replicas, more cost, same root cause

'We added three read replicas and the fork spikes disappeared—until the replicas hit their own rewrite windows simultaneously.'

— Lead SRE, after a 3 AM pager rotation

That quote captures the illusion. Offloading reads to replicas lets the primary skip heavy AOF rewrites, but each replica still forks independently. If you schedule rewrites haphazardly, you get cascading stalls across your fleet. The honest fix—sharding your dataset across more nodes with smaller per-node memory—is expensive. Each new instance costs CPU and RAM, and your connection overhead grows linearly. At what point does the ops bill outweigh the latency SLA? There is no universal answer, but I tell teams: if your fork spikes exceed 200 ms on a 32 GB instance, splitting into two 16 GB nodes often solves it cheaper than buying enterprise flash. That said, you're trading one cost (disk) for another (instance count). Choose deliberately.

So where does that leave you? You can optimize fork to the millimeter—tune the kernel, pin the child to a separate NUMA node, use lazyfree for large key deletion—but you can't escape the boundary where physics and economics meet. When you have exhausted every sysctl and still see 100+ ms spikes under peak writes, stop tuning. Start planning the architectural split. Your code will thank you at 2:47 AM.

Reader FAQ

Should I use 'appendfsync always' with AOF?

I get this question every time fork latency comes up. The answer — probably not on a production master under heavy write load. 'appendfsync always' forces an fsync on every write before the Redis server returns OK to the client. The latency multiplier is brutal: each fsync trigger can stall the event loop for 2–8 milliseconds on standard SSDs, and during a fork it compounds because the kernel is already fighting to copy memory pages. We tested this once on a bare-metal instance doing 8k writes/sec. Fork latency jumped from 120ms to 770ms. The trade-off is data safety — you lose at most one write on crash — but the operational cost is a write-heavy server that times out under any background fork. Use 'appendfsync everysec' and accept the one-second data window. If you truly need 'always', keep a separate replica that runs with that setting while your master uses 'everysec'. Painful compromise, but fewer 3 AM pages.

Does Redis 7.0's multi-threaded I/O help fork latency?

Directly? No. Multi-threaded I/O in 7.0 only parallelises network read/write — the fork itself remains single-threaded. The kernel calls fork(), the parent thread stops, COW mechanics start. Threading doesn't change that. However, there is an indirect benefit: with I/O offloaded to background threads, the main thread spends less time handling network sockets, which means you can absorb higher load before the fork hits. That matters. We saw a fork latency spike drop from 480ms on 6.2 to 210ms on 7.2 — not because the fork sped up, but because the main loop wasn't already saturated when COW started. The catch is you must tune 'io-threads' and 'io-threads-do-reads' properly, else you introduce cross-core cache invalidation overhead. Wrong order — you make things worse. Start with 2 threads, benchmark under your real write profile, and never exceed 4 on a standard server. That's the sweet zone.

What about using a replica for persistence — any gotchas?

Most teams skip this: replicate all write traffic to a follower, turn off all persistence on the master, and run AOF rewrites only on the replica. Sounds clean. The reality is trickier. The replica's fork still blocks the background save thread, and if the replica is processing replication buffers alongside client queries (yes, some setups do this), the fork latency affects replication ACK timing. We fixed this once by isolating the replica — dedicated machine, no client traffic, only replication stream. Fork latency dropped from 340ms to sub-15ms. But here's the gotcha people don't see: if the replica lags during its fork, the master's replication backlog grows. Large backlog + network glitch = full resync. That hurts. So you must increase 'client-output-buffer-limit slave' and monitor repl_backlog_size. A second pitfall: failover. If the master dies and the replica has just started a fork, you can't promote it immediately — the fork must finish. That window is a single point of failure. Plan a sentinel delay or accept a manual wait. No free lunch.

'We moved all AOF rewrites to a replica and thought we were done. Then the replica's fork took 600ms and we lost 30 seconds of replication buffer. Full resync under load — that was a bad Tuesday.'

— Engineer at a mid-size ad-tech firm, after their first fork-related outage

So yes, offloading persistence works—but test the failover scenario under your actual write load, not a synthetic benchmark. Measure replication lag during fork. If it exceeds 200ms, reconfigure or accept trade-offs.

Share this article:

Comments (0)

No comments yet. Be the first to comment!