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.

Valkey behaves the same way by default. Valkey 9.2, currently a release candidate, adds an opt-in forkless save that removes the copy-on-write spike for RDB snapshots, at the cost of slower saves and briefly blocked writers.

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?

On Redis, and on Valkey with default settings, none of these remove the mechanism. Each one narrows how often, or how badly, it bites. The one exception is Valkey 9.2's opt-in forkless save, which removes the fork for RDB snapshots specifically, and only for RDB snapshots.

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, including on Valkey 9.2 with forkless saves enabled

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

Valkey 9.2 forkless save (opt-in, release candidate)

Serializes BGSAVE and periodic saves from a background thread instead of a forked child, so no pages get duplicated

Needs a restart to enable, adds 4 bytes of memory per key, can make saves slower, and briefly blocks writers to keys not yet saved; AOF rewrites and replica full syncs still fork

Can Valkey take a snapshot without forking?

Yes, starting with Valkey 9.2, as an opt-in. Valkey 9.2.0-rc1, released on 16 September 2026, adds forkless RDB snapshots controlled by two new settings, forkless-infrastructure-enabled and bgsave-default-method. Both default to the old behavior, so an upgraded server keeps forking until you change them. At the time of writing, 9.2 is a release candidate: the project's release plan targets general availability on 15 November 2026, and setting names can still change before then.

The forkless path replaces the child process with a background thread inside the same process. There is no second address space, so there is nothing to copy-on-write. To keep the snapshot point-in-time, a write to a key the background thread has not saved yet parks that client in a new BLOCKED_INUSE state, the key is saved first, and then the command runs. Lua scripts that write keys they didn't declare can't be paused mid-script, so they block the main thread until the key is saved. These behaviors are listed in the forkless save pull request.

What it requires, per the pull request and Elestio's walkthrough:

  • forkless-infrastructure-enabled yes is immutable. It has to be in the config file or on the command line at startup, so adopting forkless saves on a running fleet means a restart.
  • While enabled, every key carries 4 extra bytes of metadata. That is small per key and adds up at scale: 500 million keys is about 2 GB.
  • bgsave-default-method forkless can be set at runtime, but only on a server started with the infrastructure flag. It applies to BGSAVE and to automatic save points.
  • Modules that register custom data types must declare forkless support. If any loaded module doesn't, Valkey falls back to a fork.
  • Progress is visible in INFO persistence through rdb_current_bgsave_type, rdb_last_bgsave_type, and forkless_estimated_seconds_remaining.

What it costs is a trade, not a free fix. One third-party test ran Valkey 9.2.0-rc1 in two Docker containers on one host, loaded 3 million 300-byte keys (about 1.04 GiB of used memory), drove eight writer threads, and triggered BGSAVE seven times per mode. It is a single author's setup on a release candidate, not a benchmark we have reproduced:

Measured during BGSAVE

fork (default)

forkless

Extra memory (cgroup delta, mean)

About 354 MB

About 10 MB

rdb_last_cow_size (mean)

About 355 MB

0

Save duration

3 seconds

5 to 6 seconds (about 70% longer)

Write throughput around the save

About 27,200 ops/sec

About 23,000 ops/sec (about 15% lower)

Worst single PING round trip (mean of runs)

About 20 ms

About 7 ms

Forkless moved the cost from memory to save duration and write throughput. If memory headroom during snapshots is what pages you at night, that is the trade you want. If the host has memory to spare and write throughput matters more, the fork default may be doing less damage than its memory graph suggests.

Two release-candidate caveats worth knowing. A maintainer's audit of 9.2 logged that a forkless save that cannot create its temp file leaves rdb_last_bgsave_status at ok, so stop-writes-on-bgsave-error never engages. Separately, the 9.2.0-rc1 release notes say active expiration now increments the dirty counter, so save points, and therefore BGSAVE, may trigger more often after an upgrade, whichever save method you use.

Does Valkey 9.2 still fork for AOF rewrites and replica syncs?

Yes. In 9.2.0-rc1 the forkless path covers RDB saves only: BGSAVE and automatic save points. AOF rewrites and replica full syncs still use a forked child, so they still carry the same copy-on-write memory risk as before, even with forkless saves enabled.

Operation

Redis

Valkey, default settings

Valkey 9.2, forkless enabled

BGSAVE and automatic save points

Forks

Forks

No fork (background thread)

AOF rewrite (BGREWRITEAOF)

Forks

Forks

Forks

Replica full sync

Forks

Forks

Forks in 9.2.0-rc1

The Redis column follows Redis's persistence documentation, which describes both RDB snapshots and AOF rewrites as fork-based. For Valkey, the third-party test above measured a nonzero aof_last_cow_size for an AOF rewrite on a forkless-enabled server, and Elestio's walkthrough notes that RC1 wires forkless into BGSAVE and periodic saves only. The Valkey project has proposed forkless full sync as further work, with details not yet final.

The practical consequence: if your memory spikes come from replicas resyncing after a disconnect, or from AOF rewrites, enabling forkless saves will not change them. Check which operation was running when memory peaked before you treat forkless saves as the fix.

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 either, and it isn't something you turn on: it is the only snapshot path Dragonfly has. Instead of splitting into a parent and child process that then diverge under copy-on-write, each shard thread serializes a versioned, point-in-time view of its own data 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.

Where it differs from Valkey 9.2's forkless path is the write that lands on an entry not yet serialized. Valkey parks that client until its background thread has saved the key. In Dragonfly's published snapshot design, the write path serializes the entry's current value into the snapshot itself and then applies the write, so the client isn't waiting on a separate thread. Both keep the snapshot point-in-time; they put the cost in different places.

Aspect

Valkey 9.2 forkless save

Dragonfly snapshots

Enabled by default

No, opt-in

Yes, the only mode

Adopting it needs a restart

Yes (immutable startup flag)

Not applicable

Write to a key not yet saved

Client blocked until the background thread saves it

Writer serializes the current value, then applies the write

Replica full sync

Still forks in 9.2.0-rc1

No fork

Release status

Release candidate, GA targeted 15 November 2026

Shipping default

Dragonfly's published benchmark measured the memory side 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). It also compares against Redis's fork-based save, not against Valkey 9.2's forkless mode; we are not aware of a published head-to-head between the two forkless approaches. 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.
  • You run Valkey 9.2 or later with forkless saves enabled, your persistence is RDB snapshots from BGSAVE or save points, and the node doesn't do AOF rewrites or serve frequent replica full syncs. Those two paths still fork.