Skip to content

RedisCacheWriter default changed from synchronous to asynchronous in 4.x without clear migration notice #3348

@hyeongguen-song

Description

@hyeongguen-song

Summary

In Spring Data Redis 4.x, RedisCacheWriter.put() now defaults to
asynchronous (fire-and-forget) writes when a ReactiveRedisConnectionFactory
(e.g., Lettuce) is present. This is a silent behavioral change from 3.x where
writes were always synchronous/blocking.

There is no mention of this change in the migration guide, making it very
difficult to diagnose cache inconsistencies after upgrading.

Environment

  • Spring Data Redis: 3.x → 4.x
  • Redis driver: Lettuce (ReactiveRedisConnectionFactory)

Problem

What changed internally

In DefaultRedisCacheWriter, the constructor now accepts an asynchronousWrites
flag, which defaults to true when a ReactiveRedisConnectionFactory is
detected:

// DefaultRedisCacheWriter.java (4.x)
if (REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT
        && connectionFactory instanceof ReactiveRedisConnectionFactory) {
    this.asyncCacheWriter = new AsynchronousCacheWriterDelegate();
    this.asynchronousWrites = asynchronousWrites; // defaults to true via nonLockingRedisCacheWriter
}
// put() in 4.x — fire-and-forget when asynchronousWrites = true
if (writeAsynchronously()) {
    asyncCacheWriter.store(name, key, value, ttl)
        .thenRun(() -> statistics.incPuts(name)); // no error handling
} else {
    execute(name, connection -> { doPut(...); return "OK"; });
}

In 3.x, put() was always a blocking call. There was no async path.

Real-world impact: cache warmup in batch applications

A common pattern is running a short-lived batch application to pre-warm the
cache before the main application starts:

Batch app starts
  → calls @Cacheable method
  → transaction commits
  → afterCommit() fires → cacheWriter.put()
  → async write is scheduled (fire-and-forget)
Batch app finishes → Spring context closes → Lettuce Netty event loop shuts down
  → in-flight async write is dropped
  → cache entry never reaches Redis
Main app starts → cache miss on every request

This behavior is non-deterministic: if the async write happens to complete
before the context closes, the cache is populated. Otherwise, it silently fails.
No exception is thrown. No log is emitted.

After upgrading from 3.x to 4.x, our cache warmup batch stopped working
intermittently — with no error in sight.

Workaround

The immediateWrites() option on RedisCacheWriterConfigurer restores
synchronous write behavior:

RedisCacheWriter writer = RedisCacheWriter.create(connectionFactory,
    config -> config.immediateWrites());

RedisCacheManager manager = RedisCacheManagerBuilder
    .fromCacheWriter(writer)
    .build();

This works, but:

  1. It is not mentioned in the migration guide from 3.x to 4.x
  2. The API surface of RedisCacheWriterConfigurer is not easy to discover
  3. Users migrating from 3.x have no reason to look for this option

Suggestion

  1. Add a note to the migration guide explaining that writes are now
    asynchronous by default when using a reactive driver, and document
    immediateWrites() as the opt-in for the previous behavior.
  2. Log a warning when an async write fails (instead of silently dropping it).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions