RDB and AOF
Verified on Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24, Apple-silicon Mac. The# cli/...transcript is replayed by the course harness; theconsoleblocks are realdocker execsessions against my own throwaway container with a volume on/data(they change files, so they are not replayed). Persistence reference: Redis persistence docs.
RDB — periodic snapshots
Section titled “RDB — periodic snapshots”RDB writes a point-in-time snapshot of the whole dataset to one compact binary file (dump.rdb). On startup Redis loads it, which makes RDB the fastest way back up after a restart.
Manual: SAVE writes the file on the command thread (everyone waits); BGSAVE forks a child that writes it while the parent keeps serving. Automatic: save <seconds> <changes> rules; a snapshot is taken when at least <changes> keys changed within <seconds>. These are the defaults of the official image, straight from the server, together with the answers that decide the next sections:
127.0.0.1:6379> CONFIG GET save1) "save"2) "3600 1 300 100 60 10000"127.0.0.1:6379> CONFIG GET appendonly1) "appendonly"2) "no"127.0.0.1:6379> CONFIG GET aof-use-rdb-preamble1) "aof-use-rdb-preamble"2) "yes"127.0.0.1:6379> CONFIG GET dbfilename1) "dbfilename"2) "dump.rdb"127.0.0.1:6379> SET greeting helloOK127.0.0.1:6379> BGSAVEBackground saving started127.0.0.1:6379> LASTSAVE(integer) 1790750454So a fresh image snapshots on the 3600 1 300 100 60 10000 rules, has no AOF (appendonly no), and would use an RDB preamble if you turned AOF on. BGSAVE replies at once with Background saving started; the save finishes later, so check for completion rather than assuming it:
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
// INFO returns "field:value" lines; turn a section into an objectasync function info(section: string) { const out: Record<string, string> = {}; for (const line of (await redis.info(section)).split('\r\n')) { const i = line.indexOf(':'); if (i > 0) out[line.slice(0, i)] = line.slice(i + 1); } return out;}
await redis.set('greeting', 'hello');const before = Number((await info('persistence')).rdb_saves);console.log(await redis.bgsave()); // Background saving started
// BGSAVE only STARTS the save: poll until it finished, then check the statuslet p = await info('persistence');while (p.rdb_bgsave_in_progress === '1' || Number(p.rdb_saves) === before) { await new Promise((r) => setTimeout(r, 50)); p = await info('persistence');}console.log('rdb_last_bgsave_status:', p.rdb_last_bgsave_status); // okconsole.log('changes since last save:', p.rdb_changes_since_last_save); // 0console.log('LASTSAVE matches INFO:', (await redis.lastsave()) === Number(p.rdb_last_save_time)); // true
await redis.quit();Real output (this Mac):
Background saving startedrdb_last_bgsave_status: okchanges since last save: 0LASTSAVE matches INFO: trueLASTSAVE is a Unix timestamp in seconds, so two saves within the same second are indistinguishable; the loop above watches rdb_saves and rdb_bgsave_in_progress from INFO persistence instead.
save 3600 1save 300 100save 60 10000dbfilename dump.rdbdir /dataThose three save rules mean: hourly if at least 1 key changed, every 5 minutes if 100 changed, every minute if 10,000 changed. save "" disables automatic snapshots. The cost of RDB is the window: anything written after the last snapshot is lost on a crash.
SAVE blocks, BGSAVE forks
Section titled “SAVE blocks, BGSAVE forks”I loaded 1,000,000 small keys, ran SAVE from one client and sent a PING from another 20 ms later:
// @runimport { Redis } from 'ioredis';
const a = new Redis(process.env.REDIS_URL!);const b = new Redis(process.env.REDIS_URL!);await a.flushall();await a.eval("for i=1,1000000 do redis.call('SET','k:'..i,'value-'..i) end return 1", 0); // 1M small keysawait b.ping();
// SAVE runs on the command thread: a second client's PING waits for itconst t0 = performance.now();const saving = a.save();await new Promise((r) => setTimeout(r, 20));const t1 = performance.now();await b.ping();const waited = performance.now() - t1;await saving;console.log(`SAVE took ${(performance.now() - t0).toFixed(0)} ms; a PING sent 20 ms later waited ${waited.toFixed(0)} ms`);
// BGSAVE forks: the reply is immediate and other clients keep being servedawait new Promise((r) => setTimeout(r, 300));const t2 = performance.now();console.log(await a.bgsave(), `in ${(performance.now() - t2).toFixed(1)} ms`);const t3 = performance.now();await b.ping();console.log(`PING right after BGSAVE: ${(performance.now() - t3).toFixed(1)} ms`);
await new Promise((r) => setTimeout(r, 500));await a.flushall();a.disconnect();b.disconnect();Real output (this Mac):
SAVE took 259 ms; a PING sent 20 ms later waited 236 msBackground saving started in 7.3 msPING right after BGSAVE: 1.2 msWhile SAVE ran, the second client’s PING waited 236 ms for a server that was busy writing. BGSAVE returned in single-digit milliseconds and the next PING took 1.2 ms. The fork is not free (it copies page tables and can add latency spikes on very large datasets, see the latency docs), but the write itself stays off the command thread.
AOF — the append-only file
Section titled “AOF — the append-only file”With appendonly yes Redis appends every write command, in the wire format, to a log and replays it on restart. appendfsync controls how often the OS is told to flush to disk: always (after every command; safest, slowest), everysec (once per second; you can lose about a second; the default) and no (the OS decides).
appendonly yesappendfilename "appendonly.aof"appendfsync everysecaof-use-rdb-preamble yesWhat is on disk
Section titled “What is on disk”Since Redis 7.0 the AOF is a directory of files (multi-part AOF), not one file. I enabled AOF on a running container, wrote a key and ran a rewrite:
$ docker exec redis-b2-persist ls -A /data(nothing yet: no snapshot, no AOF)$ docker exec redis-b2-persist redis-cli SET greeting helloOK$ docker exec redis-b2-persist redis-cli CONFIG SET appendonly yesOK$ docker exec redis-b2-persist ls -l /data/appendonlydir-rw------- 110 appendonly.aof.1.base.rdb-rw------- 0 appendonly.aof.1.incr.aof-rw------- 102 appendonly.aof.manifest$ docker exec redis-b2-persist cat /data/appendonlydir/appendonly.aof.manifestfile appendonly.aof.1.base.rdb seq 1 type bfile appendonly.aof.1.incr.aof seq 1 type i startoffset 0$ docker exec redis-b2-persist head -c 9 /data/appendonlydir/appendonly.aof.1.base.rdbREDIS0015$ docker exec redis-b2-persist redis-cli SET more 1OK$ docker exec redis-b2-persist cat /data/appendonlydir/appendonly.aof.1.incr.aof*2$6SELECT$10*3$3SET$4more$11$ docker exec redis-b2-persist redis-cli BGREWRITEAOFBackground append only file rewriting started$ docker exec redis-b2-persist ls /data/appendonlydirappendonly.aof.2.base.rdbappendonly.aof.2.incr.aofappendonly.aof.manifest$ docker exec redis-b2-persist cat /data/appendonlydir/appendonly.aof.manifestfile appendonly.aof.2.base.rdb seq 2 type bfile appendonly.aof.2.incr.aof seq 2 type i startoffset 1Read the listing like this:
appendonly.aof.1.base.rdbis the base: a snapshot of the dataset at the moment AOF was switched on. It starts with the bytesREDIS0015, so it is an RDB file.appendonly.aof.1.incr.aofis the incremental log of commands since the base, in the plain wire format shown (*3 $3 SET $4 more $1 1isSET more 1).appendonly.aof.manifestlists the parts and their sequence numbers.BGREWRITEAOFdid not rewrite a giant file: it wrote a new base (2), started a fresh incr file (2) and updated the manifest.
redis-check-aof (in the image) validates the whole set, and redis-check-rdb does the same for a snapshot:
$ docker exec redis-b2-persist redis-check-aof /data/appendonlydir/appendonly.aof.manifest (trimmed)Start checking Multi Part AOFStart to check BASE AOF (RDB format).[offset 0] Checking RDB file /data/appendonlydir/appendonly.aof.4.base.rdb[offset 143] Checksum OKRDB preamble is OK, proceeding with AOF tail...BASE AOF appendonly.aof.4.base.rdb is validStart to check INCR files.INCR AOF appendonly.aof.5.incr.aof is validAll AOF files and manifest are valid(File numbers are higher here because the container had run more rewrites and backups by then.)
Hybrid persistence is what you already get
Section titled “Hybrid persistence is what you already get”“Hybrid” means the AOF base is written in RDB format (fast to load) and only the tail is a command log. The setting is aof-use-rdb-preamble, and it is yes by default since Redis 5.0: the shipped redis.conf says aof-use-rdb-preamble no in 4.0 (with the comment that it will “at some point be used as the default”) and yes in 5.0.0. On 8.10.2 CONFIG GET aof-use-rdb-preamble is yes, and the .base.rdb above is the result. It applies whenever AOF is on; you do not need RDB snapshots to be on as well. (The earlier text of this lesson said 7.0; 7.0 only changed the file layout to multi-part.)
BACKUP: online backups (8.10+)
Section titled “BACKUP: online backups (8.10+)”Copying dump.rdb or the appendonlydir by hand was always fiddly (you must stop rewrites or copy at the right moment). Redis 8.10.0 added the BACKUP family (docs): BACKUP START takes a fresh base into backupdirname (default backupdir, under dir), BACKUP LIST shows the pinned files, BACKUP SEAL freezes the incremental part and writes a manifest, BACKUP CLEANUP releases the files, BACKUP STATUS / BACKUP ABORT inspect and cancel. It works while writes continue:
127.0.0.1:6379> BACKUP STATUS1) "state"2) "idle"3) "error"4) ""5) "start_time"6) (integer) 07) "end_time"8) (integer) 0127.0.0.1:6379> BACKUP STARTOK127.0.0.1:6379> BACKUP LIST1) "/data/backupdir/appendonly.aof.4.base.rdb"127.0.0.1:6379> SET written-during-backup 1OK127.0.0.1:6379> BACKUP SEALOK127.0.0.1:6379> BACKUP LIST1) "/data/backupdir/appendonly.aof.4.base.rdb"2) "/data/backupdir/appendonly.aof.5.incr.aof"3) "/data/backupdir/appendonly.aof.manifest"127.0.0.1:6379> BACKUP CLEANUPOK127.0.0.1:6379> BACKUP STATUS1) "state"2) "idle"3) "error"4) ""5) "start_time"6) (integer) 07) "end_time"8) (integer) 0The SET written after START is inside the sealed backup (the incr file appears in the second LIST). After SEAL you copy the three listed files to your storage, then CLEANUP. To restore, point the startup-only preload-file aof:/path/to/appendonly.aof.manifest at the manifest. This needs 8.10 or newer; on older servers use BGSAVE plus copying the file, or the documented “disable rewrites, copy, re-enable” AOF procedure. I did not test preload-file.
Reading AI-generated code
Section titled “Reading AI-generated code”An assistant adds an admin endpoint “to persist Redis now”:
🐛 Spot the bug
app.post('/admin/persist', async (_req, res) => {await redis.save(); // write dump.rdb nowres.json({ ok: true });});Show the answer
SAVE runs on the command thread. With 1,000,000 keys it took 259 ms, during which every other client, including the site’s own requests, was stuck (a PING waited 236 ms). At production sizes that is seconds of outage per click. BGSAVE forks and replies in 7 ms. Then check the outcome instead of assuming it:
await redis.bgsave(); // "Background saving started"// poll INFO persistence: rdb_bgsave_in_progress === '0' and rdb_last_bgsave_status === 'ok'The complete poll loop is the runnable persistence-expiration-rdb-aof-bgsave.ts above.
Mental Model
Section titled “Mental Model”Persistence is two different promises. RDB promises a consistent picture as of some past moment, cheap to store and fast to reload. AOF promises a running record of changes, and how much of the record survives a crash depends on appendfsync. Modern Redis stores the AOF as a picture plus a short record after it, which is why you get fast restarts and small loss windows together.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
| RDB only | Small files, fastest restart, good for backups | Loses everything since the last snapshot; BGSAVE fork can spike latency on huge datasets |
AOF (everysec) | About one second of exposure | Bigger files, replay time, fsync work |
AOF (always) | Smallest loss window | Slowest writes |
| Hybrid base (default) | RDB-speed loading with a command-log tail | Two file formats in one directory to understand |
SAVE | Simple, synchronous | Blocks all clients (259 ms at 1M keys) |
BACKUP (8.10+) | Consistent online backup with a manifest | Needs 8.10; you still copy the files off the host |
Common Misconceptions
Section titled “Common Misconceptions”- “AOF is a single file.” Since 7.0 it is a directory: base + incr + manifest.
- “Hybrid mode arrived in 7.0.”
aof-use-rdb-preamble yeshas been the default since 5.0. - “
BGSAVEreturning means the data is safe.” It means the save started; checkrdb_last_bgsave_status. - “Hybrid needs both RDB and AOF enabled.” The preamble applies whenever
appendonly yes. - “AOF with
everysecnever loses data.” It can lose about a second.
Common mistakes
Section titled “Common mistakes”SAVEon a live server — useBGSAVE.- RDB only, expecting high durability — the window is the
saveinterval. - Never testing a restore — run
redis-check-aof/redis-check-rdband actually start a server from the copy. - Copying files by hand while a rewrite runs — use the
BACKUPfamily (8.10+) or the documented procedure.
Real-world example
Redis’s own documentation compares RDB and AOF, explains the multi-part AOF and the RDB preamble, and documents the
BACKUPcommands added in 8.10 (Redis persistence).