Skip to main content

Does Redis lose data on restart? RDB, AOF and what survives

Same configuration, two ways to stop Redis: with kill -9 zero of a thousand keys survive, with an orderly shutdown all of them. Measured on 8.0.5 and 7.4.7.

What do you think about this article?

Does Redis lose data on restart? RDB, AOF and what survives
Redisfedericocalo.dev

At a Glance

Same configuration, two ways to stop Redis: with kill -9 zero of a thousand keys survive, with an orderly shutdown all of them. Measured on 8.0.5 and 7.4.7.

In short. It depends on two settings and on how Redis dies. I started two instances with the same configuration - the save rules of my development setup, AOF off - wrote a thousand keys into both and stopped them in two ways. The one killed with kill -9 came back with zero keys; the one shut down in an orderly way with a thousand. With the AOF on (appendonly yes, appendfsync everysec) even the kill -9 gave back a thousand keys, and I measured the price: the file takes 1.7 times the space, the startup of 300,000 keys goes from 330 to 620 ms, and appendfsync always, on a serial connection, collapses from 32,718 to 876 writes per second. The outputs come from commands run on 8 November 2026 on Redis 8.0.5 (native) and repeated on Redis 7.4.7 (image redis/redis-stack-server:latest). I did not verify the production configuration: where I write "this instance" I am talking about my development setup.

The wrong question and the right one

"Is Redis persistent?" has no answer: Redis is an in-memory process with two optional mechanisms for writing to disk. The question that produces a decision is asked one key at a time: if this key disappears right now, what breaks?

For the cache of a list of articles the answer is "nothing, it gets rebuilt". For the marker that invalidates the tokens issued so far it is "a user I disconnected is connected again". Same instance, same technical fate, incomparable costs.

The two switches

The first is save, which is not an interval but a list of "seconds, writes" conditions. The line of my development setup, save 3600 1 300 100 60 10000, says: save after an hour with one write, five minutes with a hundred, or one minute with ten thousand.

The second is appendonly: it records at the end of a file every command that modifies the data and replays them at restart. The first takes you back to a moment in the past, the second to an instant before the end.

The census comes before the choice

Before deciding I counted, read-only:

$ redis-cli -p 6379 dbsize
(integer) 2439
$ redis-cli -p 6379 --scan --count 1000 | sed -E 's/[0-9a-f]{6,}.*//; s/:.*//' | sort | uniq -c | sort -rn
   2426 article
     11 dataset
      1 open_meteo_archive
      1 kb_fiscal_paese_eval_green

99% are article embeddings, but this is an instance without traffic: the census that matters is done on the code, which tells which families will exist under load. In the backend I found five.

  • jwt:revoked:{jti} and tokensValidAfter:{userId} (JwtRevocationService, lines 42 and 45): losing them reopens revoked tokens until the access token expires, 900 seconds (application.yml line 304).
  • dq:idem:{scope}:{key} and dq:lock:{key} (IdempotencyService, lines 57 and 60): losing them removes the protection against double charging of credits.
  • bk-token:{token} (BookingTokenService, line 66): losing them invalidates the tokens, the only family that errs on the safe side.
  • {version}:{cacheName}::{key}, from Spring's cache manager: they are rebuilt on the first request, with a spike on the database.
  • article:*: the search service rewrites them at its own startup, in the lifespan phase (ml-service/app/main.py, line 367). They cost a restart, not a piece of data.

The criterion that holds the five lines together is not "how important the data is" but who puts it back in place if it disappears, and the answer changes from line to line: the code, a user logging in again, or nobody. Read this way, the list is a severity scale: at the top a loss with security consequences, at the bottom one that is repaired by restarting a service. A single policy must cover both, and this is where the compromise comes from.

Four restarts, four outcomes

The experiments run on a throwaway port, in temporary folders, never on the development instance. The script: a thousand SET in a pipe, a second and a half, death, restart, DBSIZE.

Case A: no persistence

$ D=$(mktemp -d)
$ redis-server --port 6399 --save "" --appendonly no --daemonize yes --dir $D
$ redis-cli -p 6399 info server | grep redis_version
redis_version:8.0.5
$ for i in $(seq 1 1000); do echo "SET k:$i v"; done | redis-cli -p 6399 --pipe
errors: 0, replies: 1000
$ redis-cli -p 6399 dbsize
(integer) 1000
$ sleep 1.6
$ kill -9 762280
$ ls -A $D            # the folder is empty: no file was written
$ redis-server --port 6399 --save "" --appendonly no --daemonize yes --dir $D
$ redis-cli -p 6399 dbsize
(integer) 0

The two options that matter are --save "", which deletes every save condition, and --appendonly no, which turns off the command log. The proof is not the final 0 but the middle line: ls -A on the data folder prints nothing, so there is no file to restart from. Remove --save "" and you are in Case C, which looks more prudent and ends the same.

Case B: AOF at one second

$ D=$(mktemp -d)
$ redis-server --port 6399 --save "" --appendonly yes --appendfsync everysec --daemonize yes --dir $D
$ for i in $(seq 1 1000); do echo "SET k:$i v"; done | redis-cli -p 6399 --pipe
errors: 0, replies: 1000
$ sleep 1.6
$ kill -9 762378
$ ls -l $D/appendonlydir
-rw-rw-r-- 1 federicocalo federicocalo    88 Nov  8 07:57 appendonly.aof.1.base.rdb
-rw-r--r-- 1 federicocalo federicocalo 30916 Nov  8 07:57 appendonly.aof.1.incr.aof
-rw-r--r-- 1 federicocalo federicocalo   102 Nov  8 07:57 appendonly.aof.manifest
$ cat $D/appendonlydir/appendonly.aof.manifest
file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i startoffset 0
$ redis-server --port 6399 --save "" --appendonly yes --appendfsync everysec --daemonize yes --dir $D
$ redis-cli -p 6399 dbsize
(integer) 1000

Same kill -9, a thousand keys recovered. And a discovery: the AOF is not a file. Since 7.0 it is a folder, appendonlydir, with a base file in RDB format, one or more incremental ones and a manifest.

Case C: my development configuration, and a kill -9

$ D=$(mktemp -d)
$ redis-server --port 6399 --save "3600 1 300 100 60 10000" --appendonly no --daemonize yes --dir $D
$ redis-cli -p 6399 config get save
save
3600 1 300 100 60 10000
$ for i in $(seq 1 1000); do echo "SET k:$i v"; done | redis-cli -p 6399 --pipe
errors: 0, replies: 1000
$ redis-cli -p 6399 info persistence | grep -E 'rdb_changes_since_last_save|rdb_last_save_time'
rdb_changes_since_last_save:1000
rdb_last_save_time:1794121073
$ sleep 1.6
$ kill -9 762500
$ ls -A $D            # empty: no save condition was reached
$ redis-server --port 6399 --save "3600 1 300 100 60 10000" --appendonly no --daemonize yes --dir $D
$ redis-cli -p 6399 dbsize
(integer) 0

The configuration promised persistence and the folder stayed empty: a thousand writes are not enough, the closest condition is "one minute and ten thousand". The documentation doesn't mince words: "Snapshotting is not very durable ... or you accidentally kill -9 your instance, the latest data written to Redis will be lost".

Case D: the same configuration, shut down properly

$ D=$(mktemp -d)
$ redis-server --port 6399 --save "3600 1 300 100 60 10000" --appendonly no --daemonize yes --dir $D
$ for i in $(seq 1 1000); do echo "SET k:$i v"; done | redis-cli -p 6399 --pipe
errors: 0, replies: 1000
$ redis-cli -p 6399 shutdown        # without NOSAVE
$ ls -l $D
-rw-rw-r-- 1 federicocalo federicocalo 8987 Nov  8 07:57 dump.rdb
$ redis-server --port 6399 --save "3600 1 300 100 60 10000" --appendonly no --daemonize yes --dir $D
$ redis-cli -p 6399 dbsize
(integer) 1000

Opposite outcome with the same configuration. The SHUTDOWN page adds the detail that matters in production: with SIGTERM and SIGINT "the same shutdown sequence is performed". A systemctl stop saves, a killed process doesn't. The flip side: without AOF and without a save directive nothing is saved even at an orderly shutdown.

The same four cases on Redis 7.4.7

$ docker run -d --rm --name redis4-persistenza redis/redis-stack-server:latest
$ docker exec redis4-persistenza redis-cli info server | grep redis_version
redis_version:7.4.7
$ docker exec redis4-persistenza bash /tmp/in747.sh
>>> A dbsize dopo: 0          # save "", appendonly no, kill -9
>>> B dbsize dopo: 1000       # appendonly yes everysec, kill -9
>>> C dbsize dopo: 0          # save "3600 1 300 100 60 10000", kill -9
>>> D dbsize dopo: 1000       # same rules, orderly SHUTDOWN

The in747.sh script is the same script as above, run inside the container on a port that doesn't leave the host; the four lines you see are the four DBSIZE after the four restarts. The repetition is not pedantry: my development is on 8.0.5 and the project documentation speaks of Redis Stack 7, and in the same series I have already found behaviors that change between the two versions. Here nothing changes, and knowing it is worth as much as the result.

How much is lost, and how much it costs to lose nothing

The three appendfsync policies

What remains to decide is how often Redis asks the operating system to bring the bytes to disk: always at every group of commands, everysec every second, no when the kernel decides. The sentence everyone quotes must be read to the end: "you may lose 1 second of data if there is a disaster".

That second I couldn't manage to lose

I tried to measure it and got a result that refutes the experiment, not the documentation. Ten thousand keys in a pipe and an immediate kill -9, three times; then three times after 1.2 seconds. Six restarts, zero losses, on both versions:

appendfsync=everysec, 10.000 SET in pipe, kill -9 SENZA attesa (3 giri)
  giro 1: in memoria prima del kill = 10000 ; sopravvissute = 10000 ; perse = 0 ; aof incrementale = 318917 byte
  giro 2: in memoria prima del kill = 10000 ; sopravvissute = 10000 ; perse = 0 ; aof incrementale = 318917 byte
  giro 3: in memoria prima del kill = 10000 ; sopravvissute = 10000 ; perse = 0 ; aof incrementale = 318917 byte

lo stesso, ma con 1,2 s di attesa fra l'ultima scrittura e il kill -9 (3 giri)
  giro 1: prima = 10000 ; sopravvissute = 10000 ; perse = 0
  giro 2: prima = 10000 ; sopravvissute = 10000 ; perse = 0
  giro 3: prima = 10000 ; sopravvissute = 10000 ; perse = 0

The explanation lies in the meaning of fsync, and understanding it saves you from trusting the wrong experiment. When Redis runs write() the bytes leave the process and land in the kernel cache: from that moment they survive its death. fsync is for the death of the machine: power, a frozen kernel, a vanished disk. A kill -9 doesn't simulate a disaster, it simulates a process dying, and they are two different failures. The practical consequence is uncomfortable: if your typical failure is a process dying, that is a bad release or the kernel picking Redis to free memory, everysec covers you as much as always. The one-second window exists, but to see it you would have to cut the power to a machine: it remains reported by the documentation, not measured by me.

The price of always, measured in two ways

First measurement: a single connection that sends a command and waits for the reply, for three seconds.

$ python3 -I writer.py 6399 log.txt     # one connection, serial SETs for 3 s
everysec    97814 scritture in 2.990 s ->   32718.1 scritture/s
always       2619 scritture in 2.991 s ->     875.6 scritture/s
no          96542 scritture in 2.992 s ->   32269.9 scritture/s

A factor of 37. Second measurement: the same server with the official tool. -n 20000 is the number of requests, -t set limits the test to writing only, and the option I didn't write is the decisive one: in the absence of -c, redis-benchmark opens fifty clients in parallel. The column to read is not the first but p50, because it is the one that feels the wait for the disk.

$ redis-benchmark -p 6399 -q -n 20000 -t set        # Redis 8.0.5
--appendonly no                               -> SET: 84388.19 requests per second, p50=0.303 msec
--appendonly yes --appendfsync everysec       -> SET: 85836.91 requests per second, p50=0.295 msec
--appendonly yes --appendfsync always         -> SET: 22099.45 requests per second, p50=2.423 msec

$ docker exec redis4-persistenza redis-benchmark -p 7001 -q -n 20000 -t set   # Redis 7.4.7
--appendonly no                               -> SET: 85470.09 requests per second, p50=0.295 msec
--appendonly yes --appendfsync everysec       -> SET: 89285.71 requests per second, p50=0.287 msec
--appendonly yes --appendfsync always         -> SET: 20986.36 requests per second, p50=2.455 msec

With fifty clients the factor drops to about 4 and the median rises from 0.3 to 2.4 ms. The two measurements don't contradict each other: with always "it means a single write and a single fsync" for a group of commands from several clients, which the documentation calls group commit. A number taken with the wrong concurrency leads to a wrong decision.

The bigger file and the slower startup

The third cost of the AOF is not the write but the wake-up. Three hundred thousand identical keys in both cases, orderly shutdown, three restarts, measuring the milliseconds until the first successful PING:

dump.rdb        = 10277887 byte
appendonlydir   = 17378021 byte

RDB giro 1: 323 ms, 300000 chiavi
RDB giro 2: 374 ms, 300000 chiavi
RDB giro 3: 324 ms, 300000 chiavi
AOF giro 1: 678 ms, 300000 chiavi
AOF giro 2: 625 ms, 300000 chiavi
AOF giro 3: 557 ms, 300000 chiavi

The AOF takes 1.69 times the snapshot and reloads in a little less than double the time. The measurements include process startup, with a resolution of 5 ms: here they are hundreds of milliseconds, on sets a thousand times larger they become minutes in which the service doesn't answer.

The five INFO persistence fields that answer

  • aof_enabled: 1 if the AOF is active right now. It is the only way to know whether the other settings matter.
  • rdb_changes_since_last_save: how many changes are not in a snapshot. It is the current risk, in writes.
  • rdb_last_save_time: the last snapshot, in epoch seconds. Convert it: it can be older than you imagine.
  • rdb_last_bgsave_status and aof_last_write_status: ok or an error. A save that fails silently is the worst.
  • aof_rewrite_in_progress and aof_last_bgrewrite_status: to check before restarting, as the migration procedure prescribes.

The five lines answer two different questions, and it is worth separating them: the first three tell how much you are risking right now, the other two whether you can trust the answer. If you have to look at only one, look at rdb_changes_since_last_save, because it is the only one expressed in the unit you care about: your writes, not a setting.

Three traps I really ran into

An appendfsync that does nothing

$ redis-cli -p 6379 config get appendfsync
appendfsync
everysec
$ redis-cli -p 6379 info persistence | grep -E '^(aof_enabled|rdb_changes_since_last_save|rdb_last_save_time):'
aof_enabled:0
rdb_changes_since_last_save:0
rdb_last_save_time:1793815385
$ redis-cli -p 6379 info server | grep uptime_in_seconds
uptime_in_seconds:306204

This is my development instance, read-only. It says appendfsync everysec and it means nothing: aof_enabled is 0, so that line governs the synchronization of a file nobody writes. In a review you read "we have the AOF at one second"; it is decoration.

The other lines are the real measurement: last save 85 hours earlier (on 2026-11-04 at 19:03), zero unsaved changes. A kill -9 now would lose nothing, but not thanks to the configuration: because nobody has written for 85 hours.

Turning the AOF on hot and then restarting

$ redis-cli -p 6399 dbsize
(integer) 1000
$ redis-cli -p 6399 config set appendonly yes
OK
$ redis-cli -p 6399 info persistence | grep -E '^(aof_enabled|aof_rewrites|aof_base_size):'
aof_enabled:1
aof_rewrites:1
aof_base_size:8987
$ ls $D/appendonlydir
appendonly.aof.1.base.rdb
appendonly.aof.1.incr.aof
appendonly.aof.manifest

# first restart: kill -9, and I start again WITH appendonly yes
dbsize dopo il riavvio CON appendonly yes: 1000

# second restart: kill -9, and I start again WITH appendonly no (hot configuration not saved)
dbsize dopo il riavvio CON appendonly no: 0
l'AOF e' ancora sul disco: appendonly.aof.1.base.rdb appendonly.aof.1.incr.aof appendonly.aof.manifest

Look at the last line: the data was there, complete, and Redis restarted empty because nobody had told it to read it. CONFIG SET changes the server, not the configuration file: to close the circle you need CONFIG REWRITE. The documentation marks the step as IMPORTANT: "If you forget this step, when you restart the server, the configuration changes will be lost and the server will start again with the old configuration, resulting in a loss of your data".

Turning the AOF on hot immediately produced a rewrite (aof_rewrites:1) with a base file of 8,987 bytes: the data already in memory. You don't start from zero, you start from a snapshot.

An incomplete tail doesn't prevent startup

$ stat -c%s $D/appendonlydir/appendonly.aof.1.incr.aof
15415
$ truncate -s -12 $D/appendonlydir/appendonly.aof.1.incr.aof
$ stat -c%s $D/appendonlydir/appendonly.aof.1.incr.aof
15403
$ redis-server --port 6399 --save "" --appendonly yes --appendfsync everysec --daemonize yes --dir $D
$ redis-cli -p 6399 dbsize
(integer) 499
$ redis-cli -p 6399 config get aof-load-truncated
aof-load-truncated
yes

Five hundred keys written and synchronized, process killed, twelve bytes removed by hand from the end of the incremental file to simulate a write interrupted mid-command: four hundred and ninety-nine keys recovered and the server is up. Identical on 7.4.7.

The behavior is intended: with aof-load-truncated set to yes, the default value, Redis truncates where the file is consistent and goes on "in order to guarantee availability after a restart". You can ask it not to start, and then you need redis-check-aof --fix. Do you prefer a service that is up but missing the last command, or a service that is down?

The check that takes thirty seconds

On an instance you don't know, these four commands are enough. None depends on the others.

  • CONFIG GET appendonly: if it answers no, every other append* setting is decoration.
  • CONFIG GET save: empty means no automatic snapshot, not even at shutdown.
  • INFO persistence: rdb_changes_since_last_save is what you would lose at this instant.
  • ls $(redis-cli config get dir | tail -1): which files really exist.

It is not a sequence to run in order: the first two say what Redis should do, the third how much you are risking while you read them, the fourth what is really on the disk. The fourth is the one that pays off most, because it is the only one that doesn't trust the configuration: in the second trap above the files were all there, and looking at them was enough to avoid a wrong diagnosis.

And if you need a fixed point right now

$ redis-cli -p 6399 dbsize
(integer) 1000
$ redis-cli -p 6399 bgsave
Background saving started
$ redis-cli -p 6399 info persistence | grep -E '^(rdb_changes_since_last_save|rdb_last_bgsave_status):'
rdb_changes_since_last_save:0
rdb_last_bgsave_status:ok
$ redis-cli -p 6399 set scritta_dopo_il_salvataggio 1
OK
$ redis-cli -p 6399 info persistence | grep '^rdb_changes_since_last_save:'
rdb_changes_since_last_save:1
$ kill -9 
$ redis-server --port 6399 --save "" --appendonly no --daemonize yes --dir $D
$ redis-cli -p 6399 dbsize
(integer) 1000
$ redis-cli -p 6399 exists scritta_dopo_il_salvataggio
(integer) 0

BGSAVE produces a snapshot on request, in a child process, and the two lines that tell the story are the two rdb_changes_since_last_save: 0 right after the save, 1 after a single write. That one is the entire warning Redis gives you. Indeed at restart a thousand keys come back and exists answers 0 on the key written afterwards. It is the right mental model for the RDB: not "the data is safe", but "up to that instant".

A comment that takes persistence for granted

 * <p>Redis is persistent (RDB/AOF) and a deploy only restarts the JVM — cached
 * entries survive across deploys. If a deploy changes the logic that produces a
 * cached value (without also changing its cache key), the old value keeps being
 * served until its TTL naturally expires

It is line 41 of infrastructure/cache/RedisCacheConfig.java. On the problem it talks about it is right: a release restarts the Java virtual machine without touching Redis, so the cache survives, and the remedy is the version prefix. But the first half-sentence, on its own, is a generalization that my development setup refutes: there appendonly is no. And the same instance hosts the revocation keys.

A twin trap lives in scripts. Two articles on this site contain lines like cp /var/lib/redis/appendonly.aof "$BACKUP_DIR/": correct up to Redis 6, wrong from 7.0. A script like that copies nothing and exits fine.

The trade-off, and what I would choose

The RDB costs little, is compact, reloads in half the time and is perfect to archive; in exchange you lose the period since the last snapshot, usually minutes. The AOF with everysec is no slower on writes, takes 1.7 times the space, doubles the startup and reduces the window to one second. always closes it and is paid for at four to forty times.

  • Pure cache with TTL: no persistence, or only RDB so as not to restart cold. An fsync for a value that gets recomputed is waste.
  • Keys whose loss is a security risk: AOF, because the cost is not a database spike but an access that shouldn't exist.
  • Protections against double charging: AOF, knowing they are worth as much as the loss window.
  • Single-use tokens: indifferent, because the loss invalidates them.
  • Indexes rebuilt at service startup: no persistence, provided the rebuild is in the code and you have read it.

It is the same criterion as the census, applied to spending: where the code rebuilds the data the AOF is a cost without a counterpart, where nobody rebuilds it the cost is paid and not discussed. The two middle lines are the only ones where the choice has consequences outside your service.

Hence the question I prefer to ask instead of prescribing: if expendable data and data that is not expendable live together, do two instances with two policies make sense? Each piece of data would pay only its own cost, at the price of one more process. This site has only one. The criterion can be measured: how many writes per second do the families you can't lose make? If they are few, a dedicated always costs almost nothing.

What you can do now

You know that "Redis loses data at restart" is not a property of Redis but the result of three things: save, appendonly and how the process is stopped. You can recognize an inert configuration, because a CONFIG SET without CONFIG REWRITE is a ticking time bomb, and to which failure the second of everysec belongs.

The previous step, atomic compare-and-delete, is in Lua, EVAL and EVALSHA; the shape of the data and what it costs is in the article on Redis data structures. What Redis does when space runs out is the next article in the series: maxmemory, eviction and OOM. For the recovery plan there is disaster recovery and on-prem backup; for use in a semantic search system, the RAG and RediSearch setup.

Can't say what would survive the next restart of your Redis? Book a call →

Frequently asked questions

If I restart Redis, do I lose data?

It depends on how you restart it and how it is configured. With my development setup's save rules and the AOF off, a kill -9 left me with zero keys out of a thousand and an orderly shutdown with all of them: with SIGTERM Redis runs the same shutdown sequence, so a systemctl stop saves and a killed process doesn't.

RDB or AOF, which is better?

RDB if the data can be rebuilt and you want a compact file with fast startup: on 300,000 keys it took 9.8 MB and restarted in about 330 ms, against 16.6 MB and 620 ms for the AOF. AOF if losing a key costs more than a recomputation. Using both, at restart Redis rebuilds from the AOF.

How much does appendfsync always cost?

It depends on concurrency, and that is why the answers online contradict each other. On a serial connection I measured 876 writes per second against 32,718 for everysec, a factor of 37; with fifty parallel clients 22,099 against 85,837 requests per second, a factor of 4. The documentation explains it: more concurrent writes enter the same fsync.

Sources

  • Redis, Redis persistence - the three appendfsync policies, the sentences quoted above on the lost second and on kill -9, the multi-part AOF since 7.0, aof-load-truncated, the hot switch and the group commit, consulted on 2026-11-08.
  • Redis, SHUTDOWN command - the equivalence between SHUTDOWN and the SIGTERM/SIGINT signals, and the instance without AOF and without save that doesn't save at shutdown, consulted on 2026-11-08.
  • Redis, CONFIG REWRITE command - what it rewrites and why it is needed after a CONFIG SET, consulted on 2026-11-08.
  • Redis, BGSAVE command - the on-demand snapshot produced by a child process, consulted on 2026-11-08.
  • My measurements, on 8 November 2026, with the commands in the blocks above: Redis 8.0.5 native and 7.4.7 (redis/redis-stack-server:latest), throwaway instances on port 6399, read-only on 6379.
  • This site's code, on 8 November 2026: JwtRevocationService.java (42, 45), IdempotencyService.java (57, 60), BookingTokenService.java (66), RedisCacheConfig.java (41), application.yml (304), ml-service/app/main.py (367).

If you open just one, open the first: it contains five of the six sentences I quoted, including the one on kill -9 and the procedure for switching to the AOF that the second trap violates. The other three pages serve to check a detail, not to understand the mechanism; the last two items are not documentation but the way to redo the measurements.

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects