Start with a data-loss budget: what Redis persistence is for
Redis keeps its entire dataset in memory. Persistence is how that dataset, or a recent version of it, gets written to disk so a restarted process can load it back instead of starting empty. Without persistence, a crash, a reboot or a deliberate restart wipes everything.
Before comparing mechanisms, settle two numbers for your workload:
- Recovery point: how many seconds or minutes of recent writes you can afford to lose. This is your data-loss budget.
- Recovery time: how long Redis can take to load data and accept traffic again after a restart.
These two numbers pull against each other. Writing to disk more often shrinks the loss window but adds I/O and CPU work during normal operation. The persistence format also affects how fast Redis can reload on startup.
Illustrative estimate (assumed numbers, not a benchmark): Writes lost ≈ write rate × loss window. Assume 2,000 writes/s and a snapshot every 300 s. A crash just before the next snapshot loses up to 2,000 × 300 = 600,000 writes.
How much this matters depends on what Redis holds:
- Pure cache that can be refilled from a primary database: the loss budget may be "everything." The concern is cold-cache load on the backend.
- Sessions, rate-limit counters or queues: a few seconds of loss may be acceptable. Losing everything usually is not.
- System of record: the loss budget approaches zero.
The options are RDB snapshots, an AOF write log, both together, or no persistence at all.
How RDB snapshots and AOF logging work
RDB: point-in-time snapshots
An RDB file is a compact binary image of the whole dataset at one moment. Redis creates it by forking a child process. The child serializes memory to a temporary file and then renames it over the old snapshot, so a crash mid-write never corrupts the previous file. Snapshots are triggered by save rules (for example, "after 60 seconds if at least 1,000 keys changed"), by an explicit BGSAVE, or by a clean shutdown when save rules are configured. Between snapshots, nothing is written to disk. A crash therefore loses every write since the last successful snapshot, and with sparse rules that can be minutes of data.
AOF: an append-only command log
With AOF enabled, each write command is appended to an in-memory buffer and then written to the log file. On startup, Redis replays the log to rebuild the dataset. Because the log records history rather than state, it grows without bound. A counter incremented a million times produces a million entries. BGREWRITEAOF fixes this by forking a child that writes the shortest command sequence that reproduces current memory. New writes are buffered during the rewrite so they aren't lost.
Replaying commands is slower than loading a binary image. With the RDB preamble enabled, the rewritten base is stored in RDB format and only later commands are logged as text, which speeds up loading considerably.
Write path
- Client sends a write command.
- Redis applies it to memory and replies.
- The write then reaches disk along one of two paths:
- RDB: a forked child snapshots memory to a temporary file, then renames it to the snapshot file.
- AOF: the command goes to the AOF buffer, is written to the AOF file, and reaches disk on the next fsync.
# redis.conf: enable both persistence mechanisms
# --- RDB snapshots ---
# Each rule is "save <seconds> <changes>". A forked child runs BGSAVE
# when any rule matches. Writes since the last successful snapshot
# are lost on a crash.
save 3600 1
save 300 100
save 60 1000
# Snapshot location. The child writes a temp file and then renames it
# over this file.
dir /var/lib/redis
dbfilename dump.rdb
# Reject writes if a background save fails, so failed persistence
# is noticed instead of silently ignored.
stop-writes-on-bgsave-error yes
# --- AOF command log ---
# Append every write command to the log and replay it on startup.
appendonly yes
appendfilename "appendonly.aof"
# When the AOF buffer is fsynced to disk:
# always = every write, everysec = once per second, no = left to the OS.
appendfsync everysec
# Run BGREWRITEAOF automatically once the AOF has grown 100% past its
# size after the last rewrite, but only after it reaches 64mb.
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# Write the rewritten base in RDB format, followed by later commands
# as text, so restarts load faster.
aof-use-rdb-preamble yes
A redis.conf excerpt that turns on RDB save rules and AOF logging, and configures fsync policy, automatic AOF rewrite thresholds and the RDB preamble.
appendfsync and what durable actually means
The appendfsync setting decides when the operating system flushes the AOF write to the physical disk. Redis acknowledges the client before or after that flush depending on this setting, so it defines how much acknowledged data a crash can erase.
appendfsync |
When fsync runs | Loss on power failure or kernel crash | Cost |
|---|---|---|---|
always |
After each write batch, before replying | Close to none | Write latency is tied to disk fsync speed |
everysec |
Once per second, on a background thread | About the last second of writes | Small; the usual default |
no |
Whenever the kernel flushes its page cache | Whatever the kernel had not yet written, often more than a second | Lowest |
The relevant lines in redis.conf are appendonly yes and appendfsync everysec. Under always, each write waits on the disk. Illustrative estimate: if one fsync takes 2 ms, the server cannot complete more than about 500 fsync-bound write batches per second, regardless of available CPU.
These guarantees assume fsync is fast and honest. On cloud or network block storage, fsync latency can be high and variable. Under everysec, a stalled disk can widen the loss window beyond one second. Under always, it causes latency spikes. Measure fsync latency on the actual volume before relying on the setting.
Persistence protects only a single node. Replication to replicas is asynchronous by default, so a primary can acknowledge a write and then fail before any replica receives it. If a replica is promoted, that write is lost even with always on the old primary. The WAIT command narrows this window by blocking until a given number of replicas confirm the write. It does not turn Redis into a strongly consistent store.
# redis.conf: AOF durability settings
# Enable the append-only file alongside or instead of RDB snapshots.
appendonly yes
# Choose exactly one fsync policy:
# always - fsync before replying to the client; latency follows the disk
# everysec - fsync once per second on a background thread; may lose ~1s
# no - let the kernel decide when to flush; cheapest, largest window
appendfsync everysec
# Illustrative estimate, not a measurement: with appendfsync always and
# an assumed 2 ms fsync, throughput caps near 1 / 0.002 s = ~500
# fsync-bound write batches per second, however much CPU is free.
# While a background AOF rewrite or RDB save runs, 'yes' skips fsync
# to avoid stalls. This widens the loss window, so keep 'no' if
# durability matters more than latency.
no-appendfsync-on-rewrite no
# Replication is asynchronous. These two directives make the primary
# refuse writes unless at least 1 replica has acknowledged within
# 10 seconds. They limit exposure but do not make replicas synchronous.
min-replicas-to-write 1
min-replicas-max-lag 10
# Per write, a client can block until replicas confirm it, e.g.:
# redis-cli SET order:1001 paid
# redis-cli WAIT 1 500 # wait up to 500 ms for 1 replica; returns count
# A return value of 0 means no replica confirmed the write. The write
# is not rolled back.
# Check whether the disk is keeping up: in INFO persistence,
# a rising aof_delayed_fsync means fsync is stalling under everysec.
# redis-cli INFO persistence
A redis.conf excerpt that enables AOF with appendfsync everysec, includes an illustrative estimate of the throughput cost of always, and adds replica-acknowledgment settings and WAIT to narrow, but not close, the window in which an asynchronous failover can lose acknowledged writes.
What survives a restart under each configuration
The outcome depends on two things: how the process stopped, and what was on disk when it did. A clean SHUTDOWN lets Redis write a final snapshot and flush the AOF. A crash or power cut does not.
| Configuration | Clean shutdown | Process crash | Host or power failure |
|---|---|---|---|
| No persistence | Empty dataset | Empty dataset | Empty dataset |
| RDB only | Everything (final save on shutdown) | Writes since last snapshot lost | Writes since last snapshot lost |
AOF, everysec |
Everything | Little or nothing (data already handed to the kernel survives) | Up to about a second or two of writes |
AOF, always |
Everything | Nothing acknowledged | Nothing acknowledged, if the disk honours fsync |
| Both enabled | Everything, loaded from AOF | Same as AOF row | Same as AOF row |
When AOF is enabled and both files exist, Redis loads the AOF. It is the more complete record. A fresher-looking RDB file is ignored.
A crash during a write can leave a truncated final command. By default, aof-load-truncated lets Redis load everything up to that point and log a warning. Corruption in the middle of the file is different: Redis refuses to start. redis-check-aof --fix repairs the file by cutting it at the first invalid command. Everything after that point is discarded, including later valid writes. Keep a copy of the original file before running it.
Restart time also differs. An RDB file is a compact binary image that loads at close to disk and deserialisation speed. A large AOF must be replayed command by command. That can take much longer, and the node serves nothing until loading finishes. This extends failover recovery and rolling deploy windows.
#!/usr/bin/env bash
# Recover a Redis node that refuses to start because its AOF is corrupted.
set -euo pipefail
DATA_DIR=/var/lib/redis
# Redis 7+ keeps a multi-part AOF in a directory. Older versions use a single
# $DATA_DIR/appendonly.aof file instead.
AOF_DIR="$DATA_DIR/appendonlydir"
AOF_FILE="$AOF_DIR/appendonly.aof.1.incr.aof" # placeholder: use the file named in the startup error
# 1. Keep an untouched copy. --fix discards everything after the first bad
# command, including later valid writes you may want to inspect by hand.
cp -a "$AOF_DIR" "$AOF_DIR.bak.$(date +%Y%m%d%H%M%S)"
# 2. Report the problem without modifying anything.
redis-check-aof "$AOF_FILE" || echo "AOF check reported errors"
# 3. Repair by truncating at the first invalid command.
# The tool shows how much will be dropped and asks for confirmation.
redis-check-aof --fix "$AOF_FILE"
# 4. Compare sizes to see how much of the tail was cut off.
ls -l "$AOF_FILE" "$AOF_DIR".bak.*/"$(basename "$AOF_FILE")"
# 5. Start Redis. Use whatever unit name your hosts use.
systemctl start redis-server
# 6. INFO still answers while the dataset loads. Other commands return LOADING.
# A large AOF replay can keep this loop running for a long time, so budget
# for it in failover and deploy timeouts.
until redis-cli INFO persistence | grep -q '^loading:0'; do
redis-cli INFO persistence | grep -E '^(loading_loaded_perc|loading_eta_seconds):' || true
sleep 5
done
echo "Dataset loaded; node is serving requests"
Backs up a corrupted AOF, repairs it with redis-check-aof --fix (which truncates at the first invalid command), then polls INFO persistence until Redis finishes loading the dataset.
Hidden costs: fork, copy-on-write memory and replication pitfalls
Both BGSAVE and AOF rewrites call fork(). The child process writes a consistent point-in-time view of the dataset while the parent keeps serving clients. The fork itself has a cost. The kernel copies the parent's page tables while the main thread is blocked, so larger datasets produce longer pauses that show up as latency spikes. INFO reports the last one as latest_fork_usec.
After the fork, parent and child share memory pages copy-on-write. Any page the parent modifies during the save is duplicated. On a write-heavy instance, resident memory can climb toward double the dataset before the child finishes. Transparent huge pages make this worse, because a small write copies a whole large page. Redis recommends disabling them.
Illustrative estimate (assumed numbers, not a benchmark): peak memory ≈ dataset + (dataset × fraction of pages written during the save). With 20 GB of data and 40% of pages touched during the save, the peak is about 20 + 8 = 28 GB. Size the host for the peak, not the dataset.
Linux overcommit policy also matters. Under the default heuristic, forking a large process can fail outright, and Redis then stops saving. Setting vm.overcommit_memory = 1 lets the fork succeed. If copy-on-write growth then exceeds physical memory, the OOM killer takes over, so you still need headroom.
Replication is not persistence. Suppose a primary runs with persistence off and an orchestrator restarts it automatically. It comes back empty, and its replicas resync to that empty dataset, wiping their own copies. Avoid that combination entirely. Either enable persistence on the primary, or disable auto-restart and promote a replica instead.
#!/usr/bin/env bash
# Pre-flight checks before relying on BGSAVE / AOF rewrite on a Linux Redis host.
# Run as root on the Redis host. Adjust REDIS_CLI for host/port/auth.
REDIS_CLI="redis-cli -h 127.0.0.1 -p 6379"
# 1. How long did the last fork() block the main thread? (microseconds)
$REDIS_CLI INFO stats | grep latest_fork_usec
# 2. How much memory did copy-on-write duplicate during the last save/rewrite?
# Use these values to size headroom for peak, not for the dataset.
$REDIS_CLI INFO persistence | grep -E 'rdb_last_cow_size|aof_last_cow_size'
# 3. Let fork() succeed under memory pressure instead of failing the save.
# This does not add memory: COW growth past physical RAM still triggers the OOM killer.
sysctl -w vm.overcommit_memory=1
echo 'vm.overcommit_memory = 1' > /etc/sysctl.d/99-redis.conf # persist across reboots
# 4. Disable transparent huge pages so a small write copies a small page, not a huge one.
cat /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/enabled # not persistent; add to boot config
# 5. Replication is not persistence: warn if a primary has both RDB and AOF off.
role=$($REDIS_CLI INFO replication | grep '^role:' | cut -d: -f2 | tr -d '\r')
save=$($REDIS_CLI CONFIG GET save | sed -n 2p)
aof=$($REDIS_CLI CONFIG GET appendonly | sed -n 2p)
if [ "$role" = "master" ] && [ -z "$save" ] && [ "$aof" = "no" ]; then
echo "WARNING: primary has no persistence. If it auto-restarts empty,"
echo "replicas will resync to the empty dataset. Enable persistence or"
echo "disable auto-restart and promote a replica instead."
fi
A host pre-flight script that reports fork latency and copy-on-write size, sets vm.overcommit_memory=1, disables transparent huge pages, and warns when a primary runs with persistence off.
Choosing RDB, AOF, both or neither by use case
Work backwards from how much data each workload can lose and how long a cold start is acceptable.
| Use case | Configuration | Worst-case loss on crash | Disk and background I/O | Configuration complexity |
|---|---|---|---|---|
| Pure cache | Persistence off, or RDB only | Everything, or writes since the last snapshot | None, or periodic snapshot writes plus a fork | Low |
| Sessions, job queues | AOF with appendfsync everysec |
About one second of writes | Continuous appends plus periodic rewrites; the file grows between rewrites | Medium |
| Data you cannot rebuild | AOF plus RDB, with snapshots copied off-host | About one second locally; off-host copies cover loss of the host | Highest: both mechanisms fork and write | High: rewrite tuning, backup jobs, restore drills |
Pure cache. If the source database can repopulate the cache, turning persistence off removes fork pauses and disk I/O completely. Use RDB only if a cold start would overload the source database. A snapshot gives you a warm restart without the cost of an append log. The tradeoff is that the restored data may be stale.
Sessions and queues. everysec keeps write latency close to in-memory speed. Losing one second of sessions forces some users to log in again, and lost queue entries can be re-enqueued if producers retry. Avoid always unless you have measured its latency on your disks.
Irreplaceable data. Use AOF to minimise loss on a restart. Use RDB as a compact artifact that is easy to copy off the host. A local file does not survive disk failure or host loss.
Alternatives. Often the safer choice is to keep the system of record in a datastore designed for durability and treat Redis as a derived copy. Then persistence affects only restart speed, not correctness.
Operational checks, backups and impact on engineering time
Confirm what is actually happening
Configuration files show intent. INFO persistence shows results. Check rdb_last_bgsave_status and, if AOF is on, aof_last_write_status and aof_last_bgrewrite_status. Each should read ok. Also check rdb_last_save_time against the clock and rdb_changes_since_last_save. A failed fork or a full disk can leave persistence silently stale for days, so alert on these fields, not on whether the process is up.
To force a snapshot, run BGSAVE, then poll until rdb_bgsave_in_progress returns to 0 and LASTSAVE has advanced. Find the file with CONFIG GET dir and CONFIG GET dbfilename. Redis writes a temporary file and renames it into place, so the RDB file is safe to copy once the save finishes. Ship it off-host on a schedule, because a snapshot on the same disk does not survive losing that disk.
Rehearse the restore
Load a copy into a scratch instance. Compare DBSIZE and spot-check known keys, then record how long the load took. That timing is your real recovery time, and the rehearsal is what proves a backup is usable.
Budget versus cost
A written data-loss budget turns incident calls into a lookup instead of a debate. Running both RDB and AOF costs disk, I/O and some tuning. Weigh that against losing data:
Illustrative estimate (assumed numbers, not a benchmark): expected annual loss = incidents/year × rebuild hours × engineers × hourly cost = 2 × 6 × 3 × $100 = $3,600. Compare that figure with the yearly cost of the extra storage and operations work.
How OneEnv can help
Disclosure: OneEnv is the product behind this blog.
OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.
Applied to redis rdb vs aof, this is where it helps:
Saves time
- Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
- One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
- Changing a shared variable is one edit and one review instead of editing every service's
.envby hand.
Saves cost
- Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
- Less duplicated configuration to maintain: one shared definition inherited by many services.
- A wrong or stale value is caught at review time by the services it affects, not by a failed deploy.
- OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.
These are qualitative benefits, not measured results — how much you save depends on how many services share the value.
Want to try it? Start at oneenv.io.