I made a Redis cache fast enough to hide a bug.
The application was a small bookstore. Every uncached request deliberately spent 1.5 seconds in PostgreSQL before returning the catalog.
Then I added cache-aside with a 180-second TTL.
The next request was fast. I inserted a new book into PostgreSQL, refreshed the page, and the book was still missing. The database was correct. Every container was healthy. The customer was reading the old catalog.
I built that failure into a Torollo roadmap on purpose.
The cache-aside path
The application checks Redis before touching PostgreSQL:
CACHE_KEY = "cache:books"
CACHE_TTL = 180
body = cache.get(CACHE_KEY)
if body is None:
cur.execute("SELECT pg_sleep(1.5);")
cur.execute("SELECT title, author FROM books ORDER BY id;")
rows = cur.fetchall()
body = render_catalog(rows)
cache.setex(CACHE_KEY, CACHE_TTL, body)
The first request misses the cache, queries PostgreSQL and stores the rendered catalog.
The next request finds cache:books and returns it without running the database query again.
That is the part every cache-aside diagram explains. The more interesting part begins when the database changes.
The infrastructure was healthy
I added a book directly to PostgreSQL:
INSERT INTO books (title, author)
VALUES ('The Phoenix Project', 'Gene Kim');
PostgreSQL now contained the new row. Redis still contained the HTML generated before the insert.
Refreshing the store returned the cached HTML, so the new book remained invisible until one of two things happened:
The 180-second TTL expired
or:
DEL cache:books
Redis was not malfunctioning. It returned exactly the value the application had stored.
The failure was in the invalidation strategy.
Checking the configuration would miss the bug
Torollo runs the web server, PostgreSQL and Redis as real Docker containers.
A weak validator could check that:
- the Redis container is running;
-
cache:booksexists; - the key has a TTL.
Every one of those checks would pass while the application displayed stale data.
The roadmap instead sends an HTTP request to the running web application and checks the response:
{
"type": "http_get_contains",
"params": {
"node": "web",
"port": 80,
"path": "/",
"expectedText": "Phoenix"
}
}
The validation fails because the user-visible result is wrong, even though the infrastructure looks healthy.
After deleting cache:books, the next request goes back to PostgreSQL, rebuilds the cached value and includes the new book. A second request confirms that Redis is serving the corrected catalog.
Availability and freshness are different failures
The application also treats Redis as an optimization.
Redis calls have short timeouts. If the cache becomes unavailable, the application falls back to PostgreSQL. The page becomes slower, but it can still respond.
That protects availability. It does not protect freshness.
A healthy cache can return stale data much faster than a database can return the correct data. Monitoring container health or request latency alone will not expose that mistake.
The behavior has to be tested.
Reproduce the failure locally
Torollo is an open-source system-design lab backed by real local containers.
Run it with:
npx torollo start
Open Learning, choose Cache-aside with Redis, and follow the exercise until the stale catalog step.
The source is available on GitHub:
https://github.com/Derssa/Torollo
When a database write changes several cached views, where do you perform invalidation in your systems: inside the write path, through an event consumer, or by accepting a bounded TTL window?

Top comments (0)