Durability
Verified on Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24, Apple-silicon Mac with Docker Desktop. Crash tests ran in my own throwaway containers (a named volume on/data,docker kill= SIGKILL); the replication test used two containers on an--internalnetwork. Numbers are from this laptop’s VM disk: read them as shapes, not as benchmarks. Setup for the files themselves is in RDB and AOF.
The appendfsync setting
Section titled “The appendfsync setting”With AOF on, appendfsync decides how often Redis tells the OS to flush the log to the physical disk:
always:fsyncafter every write batch. The smallest loss window, the highest cost.everysec:fsynconce per second, the default. You can lose about a second of writes if the machine loses power.no: never callfsync; the OS flushes when it wants (typically within about 30 seconds on Linux).
appendonly yesappendfsync everysecaof-load-truncated yesInspecting and switching it at runtime:
127.0.0.1:6379> CONFIG GET appendonly1) "appendonly"2) "no"127.0.0.1:6379> CONFIG GET appendfsync1) "appendfsync"2) "everysec"127.0.0.1:6379> CONFIG SET appendfsync alwaysOK127.0.0.1:6379> CONFIG GET appendfsync1) "appendfsync"2) "always"127.0.0.1:6379> CONFIG SET appendfsync everysecOK127.0.0.1:6379> CONFIG GET save1) "save"2) "3600 1 300 100 60 10000"127.0.0.1:6379> CONFIG GET aof-load-truncated1) "aof-load-truncated"2) "yes"127.0.0.1:6379> WAIT 1 100(integer) 0The replayed session also shows the other two facts that matter: without configuration nothing but the save rules protects the data (appendonly no), and WAIT 1 100 answers 0 because no replica is attached (more below).
What a crash really loses: the kill matrix
Section titled “What a crash really loses: the kill matrix”For each row I started a fresh container with a volume, wrote 1,000 keys, killed it, started it again and asked DBSIZE:
| Setup | How it stopped | DBSIZE after restart |
|---|---|---|
Defaults (RDB save rules, no AOF) | docker kill (SIGKILL) | 0 (/data was empty: no snapshot ever ran) |
| Defaults | docker stop (SIGTERM) | 1000 |
Defaults, BGSAVE after key 1000, then 500 more writes | docker kill | 1000 (the 500 after the snapshot are gone) |
--appendonly yes --appendfsync everysec, SIGKILL right after the last write | docker kill | 1001 (all) |
--appendfsync always, same test | docker kill | 1001 (all) |
--appendfsync no, same test | docker kill | 1001 (all) |
| AOF, then the last 4 bytes of the incr file removed (a torn write) | restart | 999 |
What to take from it:
- Default RDB-only loses everything since the last snapshot. The default rules snapshot at the earliest 60 seconds after 10,000 changes, or after an hour for one change. Nothing had triggered, so SIGKILL lost the lot.
- A graceful stop does not. On SIGTERM Redis saves first; the log shows
Saving the final RDB snapshot before exiting.,BGSAVE done, 1000 keys saved, and the keys came back. It is why a planned restart is safe and a crash is not. - The three
appendfsyncmodes were indistinguishable here, and that is the honest result.docker killkills the Redis process; the kernel still owns the data Redis had already written, so it reaches the disk anyway.appendfsynconly changes what survives when the machine dies (power loss, kernel panic), because that is when data written but not yetfsynced vanishes. I cannot reproduce that in a container, so the “about one second” foreveryseccomes from the docs, not from this test. - A torn tail is survivable.
aof-load-truncated yes(the default) loads what is intact and logs:
# Warning: short read while loading the AOF file appendonly.aof.1.incr.aof!!!# Truncating the AOF appendonly.aof.1.incr.aof at offset 30884 !!!# AOF appendonlydir/appendonly.aof.1.incr.aof loaded anyway because aof-load-truncated is enabledredis-check-aof reported the file as invalid before that, and redis-check-aof --fix truncated it to the last good command (INCR AOF ... is valid). One more subtlety I hit by accident: my first attempt filled the keys with a single Lua EVAL, which is logged as one MULTI ... EXEC block; tearing its tail loaded 0 keys, because an incomplete transaction is dropped as a whole.
What fsync costs
Section titled “What fsync costs”redis-benchmark -t set -n 30000 -r 100000 inside each container (this Mac, Docker Desktop disk), one client and 50 clients:
| Setup | 1 client | 50 clients |
|---|---|---|
| No AOF | 21,536 req/s | 158,730 req/s |
AOF appendfsync no | 23,383 req/s | 196,078 req/s |
AOF everysec | 19,417 req/s | 159,574 req/s |
AOF always | 2,671 req/s | 51,903 req/s |
everysec is within noise of no persistence; always cost about 8x with a single client (each write waits for the disk) and 3x with 50 clients, because concurrent writes share an fsync. That is the price of the smallest window. Latency at the median went from 0.047 ms to 0.343 ms (1 client) and from 0.135 ms to 0.783 ms (50 clients).
Replication is not persistence
Section titled “Replication is not persistence”Adding a replica feels like a safety net. Test it. Two containers on an internal network, a primary started with --save "" (no RDB, no AOF) and a replica following it:
$ redis-cli WAIT 1 500 # after writing 1000 keys on the primary(integer) 1 # one replica acknowledged themprimary DBSIZE 1000, replica DBSIZE 1000$ docker kill primary && docker start primary # a crash with a restart policyprimary DBSIZE 0, replica DBSIZE 0replica log: MASTER <-> REPLICA sync: Flushing old dataThe primary came back empty, the replica reconnected, did a full resync from the empty primary and flushed its own copy. Both copies were gone. A replica is a live copy of whatever the primary currently holds, including nothing. (A cluster manager that promotes the replica instead of restarting the empty primary avoids this, but that is failover, not persistence; relying on the timing is the bug.) So enable persistence on the primary and on the replicas.
WAIT numreplicas timeout blocks until that many replicas acknowledged this connection’s writes and returns how many did. Asking for 1 with no replica connected returned 0 after the timeout. It narrows the window; it does not make replication synchronous and does not survive a failover that picks a lagging replica (docs).
From Node.js: check what you need at startup
Section titled “From Node.js: check what you need at startup”A service that must not lose orders should refuse to start against a server that cannot keep them. config('GET', ...) and wait():
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
// fail fast at startup when the server does not have the durability this service needsasync function assertDurability(need: { aof: boolean }) { const [, appendonly] = await redis.config('GET', 'appendonly') as string[]; const [, appendfsync] = await redis.config('GET', 'appendfsync') as string[]; const [, save] = await redis.config('GET', 'save') as string[]; console.log({ appendonly, appendfsync, save }); if (need.aof && appendonly !== 'yes') throw new Error('this service needs AOF (appendonly yes)');}try { await assertDurability({ aof: true }); } catch (e) { console.log('startup check:', (e as Error).message); }
// WAIT n ms: how many replicas acknowledged everything written so far by THIS connection. With no replicas it is 0await redis.set('order:1', 'paid');console.log('replicas that acknowledged:', await redis.wait(1, 100));
await redis.quit();Real output on the default image (this is what a service would see against an unconfigured server):
{ appendonly: 'no', appendfsync: 'everysec', save: '3600 1 300 100 60 10000'}startup check: this service needs AOF (appendonly yes)replicas that acknowledged: 0Managed services may forbid CONFIG GET; then check through your provider’s settings or INFO persistence (aof_enabled).
Reading AI-generated code
Section titled “Reading AI-generated code”An assistant writes a docker-compose.yml for “a highly available Redis”. Persistence is switched off because “the replica already keeps a copy”:
🐛 Spot the bug
services:redis-primary: image: redis:8 command: redis-server --save "" --appendonly no # the replica is our backup restart: alwaysredis-replica: image: redis:8 command: redis-server --replicaof redis-primary 6379Show the answer
restart: always turns any crash into “primary restarts empty”. The replica then does what replicas do: it resyncs from the primary and flushes its own data. In the two-container test above both had 1,000 keys before the crash and 0 after. Give the primary a real persistence setup and a volume, keep it on the replica too, and test by killing it:
command: redis-server --appendonly yes --appendfsync everysec volumes: ["redis-data:/data"]Mental Model
Section titled “Mental Model”Durability is how far back the disk is from memory when the lights go out. RDB-only leaves the gap at “since the last snapshot”, AOF everysec at about a second, always at almost nothing. A replica is another copy of memory, not another kind of disk. And everything above assumes the disk is a volume that outlives the container.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
| RDB only | No per-write cost; compact backups | Crash loses everything since the last snapshot (0 of 1000 keys in our SIGKILL test); planned stops are safe |
AOF everysec | About one second of exposure at near-zero cost (159k vs 159k req/s) | Larger files; still loses up to a second on power failure |
AOF always | Smallest loss window | 2,671 req/s vs 21,536 with one client (8x slower) |
AOF no | Fastest AOF | Data loss on power failure is up to the OS flush interval |
| Replica as the only safety net | Fast failover | Primary restarts empty and wipes the replica |
WAIT | Raises confidence a write reached a replica | Blocks the caller; not a guarantee across failover |
Common Misconceptions
Section titled “Common Misconceptions”- “
appendfsync alwaysis needed to survivekill -9.” All three modes kept every key in the SIGKILL test;fsyncmatters for machine failure. - “A replica means I do not need persistence.” A restarted empty primary made the replica flush its data.
- “Redis loses everything on a bad AOF.” A torn tail loads with
aof-load-truncated(999 of 1000 keys) andredis-check-aof --fixrepairs the file. - “Stopping a default container loses the data.”
docker stop(SIGTERM) saved a final snapshot; SIGKILL and OOM kills do not. - “
WAITmakes writes synchronous.” It reports how many replicas acknowledged; it does not undo the write.
Common mistakes
Section titled “Common mistakes”- No volume on
/data— the files vanish with the container even if persistence is on. - Trusting a replica instead of persistence — see the exercise.
- Never killing the process to test recovery — do it once before you depend on it.
appendfsync alwayswithout measuring — 8x fewer writes per second with one client on this machine.
Real-world example
Redis’s own documentation compares RDB, AOF and
appendfsyncand states the “about one second” loss window foreverysec(Redis persistence); replication semantics andWAITare in Replication.