Dragonfly

Why Does Redis Memory Spike During BGSAVE?

Redis's BGSAVE forks the process and leans on copy-on-write to snapshot a moving dataset. That mechanism, not a bug, is why memory can spike toward double your dataset size under write load.

The short answer

Redis's BGSAVE forks the process, then uses copy-on-write to write a stable snapshot to disk while the parent keeps serving writes. Every write to an already-shared page forces the kernel to duplicate that page in memory. In a write-heavy instance, that can push memory usage close to double a dataset's normal size before the snapshot finishes.

Why does Redis fork when it saves a snapshot?

BGSAVE needs a stable, unmoving view of the dataset to write to disk, but the dataset keeps changing while that write happens. Redis solves this with the Linux fork() system call: it creates a child process that starts out sharing every memory page with the parent. The child walks that frozen view and serializes it into the RDB file. The fork itself doesn't touch the dataset, so the parent keeps handling client commands with essentially no interruption.

The fork() call is fast relative to dataset size because it duplicates the process's page table, not the data itself. Redis's own documentation walks through the arithmetic: on Linux/AMD64, a 24 GB instance needs a roughly 48 MB page table, and copying that page table is what fork() actually pays for. On modern hardware that costs single-digit to low-double-digit milliseconds per GB; the same call was measurably slower on older Xen-based virtualization, which is largely a legacy concern on current cloud instance types.

Why does memory use go up if fork() doesn't copy any data?

fork() shares pages, it doesn't copy them, so physical memory usage doesn't move at the moment of the fork. The increase comes afterward, from copy-on-write: the kernel duplicates a page only when either process writes to it. The child process never writes, it only reads and serializes, so every extra byte of memory comes from the parent's own write traffic during the snapshot window. Each SET, HSET, or key expiry that touches an already-shared page forces the kernel to copy that page before the write can land, and the copy stays resident until the child process, and the snapshot, finish.

Why do write-heavy workloads make the spike worse?

If nothing writes to Redis while BGSAVE runs, no pages get duplicated and memory doesn't move. The spike is a direct function of how much of the dataset gets touched before the child finishes serializing it. Dragonfly's own writeup on this mechanism walks through the worst case with a concrete example: a 40 GB Redis instance can, in principle, need an additional 40 GB to duplicate every page, 80 GB in total, if write throughput is high enough that nearly every page gets touched before the snapshot completes. If throughput is lower, the snapshot may finish before all pages are duplicated, landing the peak somewhere between the two extremes.

The determining factor isn't dataset size on its own, it's how much of that dataset gets rewritten inside the time it takes the child process to finish. A large, mostly-static dataset with a fast snapshot can see almost no spike. A smaller, heavily-written dataset with a slow disk behind it can approach the full doubling.

How much memory headroom do I actually need?

Treat copy-on-write as bounded between two cases. Best case: no writes land during the snapshot, and memory doesn't move at all. Worst case: nearly the entire working set gets rewritten before the child finishes, and you pay close to double your steady-state resident memory. Real workloads sit somewhere between those two, and where they sit depends on your write rate and how long the snapshot takes.

Redis exposes the actual numbers for your instance instead of a generic rule of thumb. INFO persistence reports rdb_last_cow_size, the bytes actually duplicated by copy-on-write during the last save, and INFO stats reports latest_fork_usec, how long the fork() call itself took. Trigger a BGSAVE under representative write load and read both fields afterward. That measured rdb_last_cow_size is the real headroom your instance needed for that run, which is a better planning number than assuming a fixed percentage across every workload.

One setting changes this math directly: transparent huge pages. With THP enabled, the kernel manages memory in 2 MB pages instead of 4 KB pages, so a single write anywhere inside a 2 MB region forces the kernel to duplicate the whole region, not just the 4 KB actually written. Redis's own operations documentation recommends disabling THP for exactly this reason, alongside ensuring swap is configured and maxmemory is set so Redis reports an error rather than getting killed when it runs out of room.

What are the standard mitigations, and what do they actually cost you?

None of these remove the mechanism. Each one narrows how often, or how badly, it bites.

Mitigation

What it does

The tradeoff

Disable transparent huge pages

Removes the huge-page multiplier on copy-on-write

Doesn't remove the underlying doubling risk, only the amplification on top of it

Run BGSAVE from a replica

Keeps the fork and its memory cost off the primary

Adds a replication topology to operate, and the replica needs its own headroom to absorb the same spike

Schedule saves for low-write windows

Fewer pages get touched before the snapshot finishes

Only helps if such a window reliably exists; does nothing under constant write load

Switch to AOF instead of RDB

Avoids the periodic BGSAVE spike for normal writes

BGREWRITEAOF also forks to compact the file, so the same copy-on-write risk returns on every rewrite

Provision RAM for roughly 2x the dataset

Removes the out-of-memory risk outright

Pure cost: most of that memory sits idle outside snapshot windows

Snapshot less often

Fewer spikes over the instance's lifetime

Raises your recovery point objective, so a crash between snapshots loses more data

Should I just disable Redis persistence to avoid this?

Turning off persistence removes the BGSAVE-shaped memory spike, but it doesn't remove the underlying decision, it just moves it. You're trading a memory risk during snapshots for total data loss on any crash, restart, or host failure. For most stateful workloads that's a worse trade than the one it replaces. If persistence is already off because the workload is a pure cache sitting in front of an authoritative store elsewhere, that's a legitimate architecture, not a fix for this problem, see the last section below.

How does Dragonfly avoid this?

Dragonfly's snapshotting doesn't fork at all. Instead of splitting into a parent and child process that then diverge under copy-on-write, Dragonfly serializes a versioned, point-in-time view of each shard directly from the running process and pushes entries to the serialization sink incrementally as it goes. That gives it a natural back-pressure mechanism instead of an all-at-once memory duplication problem, and it holds regardless of dataset size.

Dragonfly's published benchmark measured this directly: fill both stores with roughly 5 GB of data (debug populate 5000000 key 1024), send update traffic with memtier_benchmark, then trigger bgsave. Dragonfly was already about 30% more memory-efficient than Redis at idle and showed no visible increase in memory use during the snapshot phase. Redis's memory use at peak reached almost 3x Dragonfly's memory use in that same run, and Dragonfly finished its snapshot within a few seconds.

That figure is Dragonfly's own reported result, not an independent third-party audit, and the published methodology doesn't state the exact instance type used for this specific test (the throughput benchmarks in the same source specify instance types like m5.large and c6gn.16xlarge; the memory test does not). Anyone validating this for a purchase decision should reproduce it against their own instance type and write pattern using the command above rather than treating the ratio as fixed.

When is this not your problem?

  • Persistence is already off, and durability lives somewhere else on purpose, for example a pure cache in front of a database of record where a cold cache on restart is acceptable.
  • Your dataset is small relative to your instance's headroom. A 2 GB Redis instance on a host with 32 GB free will rarely notice a doubling, even in the worst case.
  • Write volume is low or bursty with real idle windows between bursts. Copy-on-write only duplicates pages that get written, so a low write rate keeps the spike small no matter how large the dataset is.
  • Your managed service already prices this in. AWS ElastiCache, for instance, reserves 25% of instance memory by default for background tasks like snapshots, so the headroom is baked into the node size you're billed for rather than something you have to plan yourself. That's a cost decision already made on your behalf, not evidence that the underlying mechanism went away.