Skip to content

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 --internal network. 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.

With AOF on, appendfsync decides how often Redis tells the OS to flush the log to the physical disk:

  • always: fsync after every write batch. The smallest loss window, the highest cost.
  • everysec: fsync once per second, the default. You can lose about a second of writes if the machine loses power.
  • no: never call fsync; the OS flushes when it wants (typically within about 30 seconds on Linux).
config/persistence-expiration-durability-fsync.conf
appendonly yes
appendfsync everysec
aof-load-truncated yes

Inspecting and switching it at runtime:

cli/persistence-expiration-durability-1.redis
127.0.0.1:6379> CONFIG GET appendonly
1) "appendonly"
2) "no"
127.0.0.1:6379> CONFIG GET appendfsync
1) "appendfsync"
2) "everysec"
127.0.0.1:6379> CONFIG SET appendfsync always
OK
127.0.0.1:6379> CONFIG GET appendfsync
1) "appendfsync"
2) "always"
127.0.0.1:6379> CONFIG SET appendfsync everysec
OK
127.0.0.1:6379> CONFIG GET save
1) "save"
2) "3600 1 300 100 60 10000"
127.0.0.1:6379> CONFIG GET aof-load-truncated
1) "aof-load-truncated"
2) "yes"
127.0.0.1:6379> WAIT 1 100
(integer) 0

The 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:

SetupHow it stoppedDBSIZE after restart
Defaults (RDB save rules, no AOF)docker kill (SIGKILL)0 (/data was empty: no snapshot ever ran)
Defaultsdocker stop (SIGTERM)1000
Defaults, BGSAVE after key 1000, then 500 more writesdocker kill1000 (the 500 after the snapshot are gone)
--appendonly yes --appendfsync everysec, SIGKILL right after the last writedocker kill1001 (all)
--appendfsync always, same testdocker kill1001 (all)
--appendfsync no, same testdocker kill1001 (all)
AOF, then the last 4 bytes of the incr file removed (a torn write)restart999

What to take from it:

  1. 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.
  2. 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.
  3. The three appendfsync modes were indistinguishable here, and that is the honest result. docker kill kills the Redis process; the kernel still owns the data Redis had already written, so it reaches the disk anyway. appendfsync only changes what survives when the machine dies (power loss, kernel panic), because that is when data written but not yet fsynced vanishes. I cannot reproduce that in a container, so the “about one second” for everysec comes from the docs, not from this test.
  4. A torn tail is survivable. aof-load-truncated yes (the default) loads what is intact and logs:
Terminal window
# 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 enabled

redis-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.

redis-benchmark -t set -n 30000 -r 100000 inside each container (this Mac, Docker Desktop disk), one client and 50 clients:

Setup1 client50 clients
No AOF21,536 req/s158,730 req/s
AOF appendfsync no23,383 req/s196,078 req/s
AOF everysec19,417 req/s159,574 req/s
AOF always2,671 req/s51,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).

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:

Terminal window
$ redis-cli WAIT 1 500 # after writing 1000 keys on the primary
(integer) 1 # one replica acknowledged them
primary DBSIZE 1000, replica DBSIZE 1000
$ docker kill primary && docker start primary # a crash with a restart policy
primary DBSIZE 0, replica DBSIZE 0
replica log: MASTER <-> REPLICA sync: Flushing old data

The 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():

src/persistence-expiration-durability-check.ts
// @run
import { 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 needs
async 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 0
await 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: 0

Managed services may forbid CONFIG GET; then check through your provider’s settings or INFO persistence (aof_enabled).

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: always
redis-replica:
image: redis:8
command: redis-server --replicaof redis-primary 6379
Show 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"]

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.

ChoiceBenefitCost
RDB onlyNo per-write cost; compact backupsCrash loses everything since the last snapshot (0 of 1000 keys in our SIGKILL test); planned stops are safe
AOF everysecAbout 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 alwaysSmallest loss window2,671 req/s vs 21,536 with one client (8x slower)
AOF noFastest AOFData loss on power failure is up to the OS flush interval
Replica as the only safety netFast failoverPrimary restarts empty and wipes the replica
WAITRaises confidence a write reached a replicaBlocks the caller; not a guarantee across failover
  • “appendfsync always is needed to survive kill -9.” All three modes kept every key in the SIGKILL test; fsync matters 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) and redis-check-aof --fix repairs the file.
  • “Stopping a default container loses the data.” docker stop (SIGTERM) saved a final snapshot; SIGKILL and OOM kills do not.
  • “WAIT makes writes synchronous.” It reports how many replicas acknowledged; it does not undo the write.
  • 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 always without measuring — 8x fewer writes per second with one client on this machine.

Real-world example

Redis’s own documentation compares RDB, AOF and appendfsync and states the “about one second” loss window for everysec (Redis persistence); replication semantics and WAIT are in Replication.

Which appendfsync mode has the smallest loss window when the machine loses power?
Which appendfsync mode is the default and a common choice?
You write 1000 keys to a default Redis (no AOF) and docker kill it. What does DBSIZE say after the restart?
Why did all three appendfsync modes keep every key after docker kill?
A primary with persistence off is restarted by restart: always. What happens to its replica's data?
The last 4 bytes of the AOF incr file were torn off in a crash. With the default aof-load-truncated yes, Redis...
You call WAIT 1 100 and get 0. What does that mean?