Redis persistence: RDB, AOF and when data loss occurs

1. Overview

Redis keeps all data in RAM. If the process dies, data only survives if Redis previously wrote it to disk. This article explains two persistence mechanisms: RDB (snapshot) and AOF (append-only file), how to choose and configure them. The article also lists scenarios where you still lose data even with persistence enabled. At the end is Spring Boot code to check persistence status and a test running with Testcontainers.

2. RDB: periodic snapshots

RDB writes the entire dataset to a binary file, by default dump.rdb, at a point in time. Configuration in redis.conf:

# Snapshot if after 3600s there is at least 1 change,
# after 300s at least 100 changes, or after 60s at least 10000 changes
save 3600 1 300 100 60 10000

dbfilename dump.rdb
dir /data

# Reject write commands if the last BGSAVE failed
stop-writes-on-bgsave-error yes
rdbcompression yes

When a save condition is met, Redis calls fork(). The child process writes the snapshot to a temporary file, then renames it to dump.rdb. The parent process continues serving clients normally. Thanks to the operating system’s copy-on-write mechanism, both processes share memory pages. A page is only copied when the parent process modifies it.

You can also trigger a snapshot manually:

redis-cli BGSAVE
redis-cli LASTSAVE
Background saving started
(integer) 1760169600

BGSAVE runs in the background. LASTSAVE returns the Unix timestamp of the last successful snapshot. Avoid using SAVE in production: this command blocks the entire server until writing is complete.

Advantages of RDB:

  • Compact files, great for backups or copying to other environments.
  • Fast restarts since Redis only needs to load one binary file.
  • Minimal impact on throughput between snapshots.

Disadvantages: all changes after the last snapshot are lost if Redis crashes. With the configuration above, in the worst case you can lose up to several minutes of data.

Note: fork() on large datasets (tens of GB) can cause Redis to pause for hundreds of milliseconds. If there are many writes during snapshot, copy-on-write can nearly double RAM usage. Leave extra memory and enable vm.overcommit_memory = 1 on Linux.

3. AOF: logging every write command

AOF writes every command that modifies data (SET, INCR, HSET…) to the end of a log file. On startup, Redis replays this log to rebuild the dataset.

appendonly yes
appenddirname "appendonlydir"
appendfilename "appendonly.aof"

# always | everysec | no
appendfsync everysec

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes

3.1. Appendfsync policy

Writing to a file doesn’t mean data is on disk yet. Data may remain in kernel page cache until an fsync() call is made. This is where you decide how much data loss you can tolerate:

Value When fsync Max loss on crash Performance
always After every write command Nearly zero Slowest
everysec Every second, on background thread Around 1 second (possibly up to 2) Good, default
no OS decides, usually around 30 seconds Tens of seconds Fastest

With everysec, if the previous fsync hasn’t completed after 2 seconds due to slow disk, Redis blocks write commands to prevent further delays. Therefore, the actual data loss window can be 2 seconds, not 1.

You should also distinguish between two types of failures. If only the Redis process crashes, data that was write()’d still sits in page cache and the kernel will eventually write it to disk. If the machine loses power or the kernel panics, the part not yet fsync’d will be lost.

3.2. AOF rewrite and multi-part AOF

AOF grows over time. For example, 1 million INCR counter commands can be replaced with a single SET counter 1000000 command. Redis automatically rewrites AOF when the file is twice the size of the last rewrite (auto-aof-rewrite-percentage 100) and reaches at least 64 MB.

Since Redis 7, AOF resides in the appendonlydir directory and consists of multiple files:

appendonlydir/
├── appendonly.aof.1.base.rdb    # base, RDB format thanks to aof-use-rdb-preamble
├── appendonly.aof.1.incr.aof    # write commands generated after base
└── appendonly.aof.manifest      # describes which files belong to current AOF

With aof-use-rdb-preamble yes, the base file is written in RDB format for fast loading. The incremental part is still command logs so data loss is minimal. This is the “hybrid” mode and is the default since Redis 4.0.

4. Which mode should you choose?

Requirement Suggested Configuration
Pure cache, data can be rebuilt from DB Disable both: save "", appendonly no
Can tolerate losing a few minutes, need compact backups RDB only
Sessions, shopping carts, rate limits need durability AOF everysec + RDB for backup
Cannot lose any command (rare) AOF always, but consider using a different DB

The reasonable choice for most applications is to enable both: AOF everysec to reduce loss, RDB for periodic backup files. When both are enabled, Redis prioritizes loading AOF on startup because AOF is more complete.

Note: if you only use Redis as a cache, disable persistence. Enabling RDB for a cache only wastes fork(), disk I/O, and RAM with no benefit.

5. Checking persistence from Spring Boot application

Add dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

The following class reads persistence status, requests a snapshot, and waits for replica confirmation:

@Component
public class PersistenceInspector {

    private final StringRedisTemplate template;

    public PersistenceInspector(StringRedisTemplate template) {
        this.template = template;
    }

    public Properties persistenceInfo() {
        return template.execute((RedisCallback<Properties>) conn ->
                conn.serverCommands().info("persistence"));
    }

    public void triggerSnapshot() {
        template.execute((RedisCallback<Object>) conn ->
                conn.execute("BGSAVE", "SCHEDULE".getBytes()));
    }

    public long waitForReplicas(int replicas, long timeoutMs) {
        Long acked = template.execute((RedisCallback<Long>) conn ->
                (Long) conn.execute("WAIT",
                        String.valueOf(replicas).getBytes(),
                        String.valueOf(timeoutMs).getBytes()));
        return acked == null ? 0 : acked;
    }
}

Explanation of each method:

  • persistenceInfo() calls INFO persistence and returns a Properties containing fields like aof_enabled, rdb_last_bgsave_status.
  • triggerSnapshot() uses BGSAVE SCHEDULE. If an AOF rewrite is running, Redis will schedule the snapshot instead of returning an error.
  • waitForReplicas() calls WAIT and returns the number of replicas that have received the previous write commands. Section 6 explains why you need this command.

Part of INFO persistence output when AOF is enabled:

loading:0
rdb_changes_since_last_save:42
rdb_bgsave_in_progress:0
rdb_last_save_time:1760169600
rdb_last_bgsave_status:ok
aof_enabled:1
aof_rewrite_in_progress:0
aof_last_bgrewrite_status:ok
aof_last_write_status:ok

Three fields you should include in monitoring and alert when not ok: rdb_last_bgsave_status, aof_last_write_status, and aof_last_bgrewrite_status. The rdb_changes_since_last_save field tells you how many changes you’ll lose if relying only on RDB.

5.1. Test with Testcontainers

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class RedisPersistenceTest {

    @Container
    @ServiceConnection(name = "redis")
    static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.4"))
            .withExposedPorts(6379)
            .withCommand("redis-server", "--appendonly", "yes", "--appendfsync", "everysec");

    @Autowired
    StringRedisTemplate template;

    @Autowired
    PersistenceInspector inspector;

    @Test
    void aofShouldBeEnabledWithEverySecPolicy() {
        template.opsForValue().set("order:1001", "PAID");

        Properties info = inspector.persistenceInfo();
        List<String> fsync = template.execute((RedisCallback<List<String>>) conn ->
                conn.serverCommands().getConfig("appendfsync")
                        .values().stream().map(Object::toString).toList());

        assertThat(info.getProperty("aof_enabled")).isEqualTo("1");
        assertThat(info.getProperty("aof_last_write_status")).isEqualTo("ok");
        assertThat(fsync).containsExactly("everysec");
    }

    @Test
    void backgroundSnapshotShouldSucceed() throws InterruptedException {
        template.opsForValue().set("customer:42", "Lan");
        long before = Long.parseLong(inspector.persistenceInfo().getProperty("rdb_last_save_time"));

        Thread.sleep(1100); // ensure new timestamp is different from old timestamp
        inspector.triggerSnapshot();

        Properties info = inspector.persistenceInfo();
        for (int i = 0; i < 50 && Long.parseLong(info.getProperty("rdb_last_save_time")) <= before; i++) {
            Thread.sleep(100);
            info = inspector.persistenceInfo();
        }

        assertThat(info.getProperty("rdb_last_bgsave_status")).isEqualTo("ok");
        assertThat(Long.parseLong(info.getProperty("rdb_last_save_time"))).isGreaterThan(before);
    }
}

The first test starts Redis 7.4 with AOF everysec and confirms the configuration is in effect. @ServiceConnection automatically points spring.data.redis.host/port to the container, so you don’t need to declare any additional properties. The second test waits up to 5 seconds for rdb_last_save_time to increase, then checks if the snapshot succeeded.

6. When data is still lost?

Enabling persistence is not enough. Below are the most common scenarios in production.

1. Crash between snapshots. If using only RDB, you lose everything written after rdb_last_save_time.

2. Fsync window. With AOF everysec, you lose around 1–2 seconds when the machine crashes. With no, this can go up to tens of seconds.

3. Failover with async replication. Master returns OK to client before replica receives the command. If master dies and Sentinel or Cluster promotes a replica to master, commands not yet replicated will be lost, even if master has AOF always. WAIT helps reduce this risk:

template.opsForValue().set("payment:7788", "CAPTURED");
long acked = inspector.waitForReplicas(1, 100);
if (acked < 1) {
    log.warn("payment:7788 has not been acknowledged by replica");
}

WAIT 1 100 blocks up to 100 ms until at least 1 replica acknowledges. WAIT still doesn’t make Redis a strongly consistent system: replica may receive the command but hasn’t fsync’d yet. Think of this as reducing the probability of data loss, not a guarantee.

4. Master restarts with empty dataset. Suppose master has persistence disabled, auto-restarts, and comes back with empty data. Replica will sync with master and wipe its own data. If master has no persistence, don’t let it auto-restart.

5. Container doesn’t mount volume. Redis still writes dump.rdb and AOF, but to container filesystem. When container is deleted and recreated, those files are lost.

services:
  redis:
    image: redis:7.4
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis-data:/data
volumes:
  redis-data:

On Kubernetes, the equivalent situation is using emptyDir instead of PersistentVolumeClaim.

6. Disk full or fork() fails. When BGSAVE fails and stop-writes-on-bgsave-error yes is enabled, Redis rejects all write commands with MISCONF error. This protects data but breaks your application. Many people react by disabling this option, after which Redis continues without persistence.

7. Accidentally running FLUSHALL. With AOF, FLUSHALL is just one line at the end of the incr file. If AOF hasn’t been rewritten, you can stop Redis, delete that line, and restart. If rewrite has run, there’s no recovery path. Therefore, rename or block this command via ACL in production.

8. Eviction. This is not a persistence failure, but the result is the same. With maxmemory-policy allkeys-lru, Redis will evict keys when RAM is full. Data that cannot be lost needs noeviction.

Note: don’t use Redis as the sole store for financial or order data. If you need to guarantee “once OK is returned, nothing is lost”, use a relational database or consensus system. Redis should serve as cache or acceleration layer.

7. Conclusion

RDB provides compact backup files and fast startup, but can lose several minutes of data. AOF with everysec reduces loss to around 1–2 seconds, trading off extra I/O. Hybrid mode (aof-use-rdb-preamble) combines the advantages of both. Most real-world risks lie outside these two mechanisms: async replication, unmounted volumes, full disk, FLUSHALL, and eviction. Monitor fields in INFO persistence, use WAIT for important write commands, and test configuration with Testcontainers as shown in section 5.

redispersistencerdbaofspring-boot