Vol. IV · No. 04 Monday · 29 June 2026
Now writing — Why Your Index Scan Is Slower Than a Sequential Scan: When the Planner Is Right to Ignore Your Index dispatches · 3 streams
← All dispatches
engineering Dispatch 4 min read · 30 Apr 2026

Database-First Caching: Why Adding Redis Often Makes Things Slower

The reflex when an application gets slow is to add a cache, and the reflex when adding a cache is to reach for Redis. Both reflexes are often wrong. The database is faster than most developers think, and the cache is slower than they assume.

engineering · Curiosity

The first time an application gets noticeably slow, the team has a familiar conversation. The database is the bottleneck, someone says. We need to add caching. We should put Redis in front of the slow queries. Two weeks later there is a new component in the deployment, a new failure mode in the monitoring, a new authentication credential in the secrets store, and the application is faster. Or it is the same speed. Or it is faster on average and slower at the tails. Or it is faster for a month and then mysteriously slows down and nobody can figure out why.

The reflexive reach for Redis (or Memcached, or any external cache) is one of the most common premature optimizations in backend engineering. Sometimes it is the right answer. Often it is not, and the team would have been better off spending the same effort understanding why the database was slow and fixing it directly. The cache addition delivers a short-term speedup that masks the underlying problem and accumulates a different kind of complexity.

What the database actually does

The intuition that the database is slow comes from a specific experience: a query that scans many rows returns slowly. From there, the leap to "the database is slow" generalizes one slow query into a property of the whole system. The actual situation is usually that one or two queries are slow and most queries are fast, and the slow ones are slow because they are missing an index, or returning more data than is needed, or executing a plan that the optimizer chose poorly.

A modern database serving a primary-key lookup against an in-memory page returns in tens of microseconds. A simple indexed query returns in hundreds of microseconds. The query that takes 500 milliseconds is doing real work: scanning a large index, joining several tables, doing an aggregation that requires sorting. Cache layers do not make slow queries fast; they avoid running them. If the underlying query can be made fast (by adding an index, by restructuring the query, by denormalizing), the cache becomes unnecessary.

The Redis round-trip

The other side of the equation is what Redis actually costs. A Redis call from the application goes over the network: serialize the request, send it through the kernel, transit the network, server-side deserialize, lookup, server-side serialize, transit, kernel, deserialize. On the same machine that is sub-millisecond; across a data center it is one to two milliseconds; across regions it is tens of milliseconds.

If the database query you would have run instead takes 200 microseconds (because it is well-indexed), the Redis lookup is slower than the database. If the query takes 5 milliseconds, the cache wins, but only by a few milliseconds. The cache has to absorb a meaningful fraction of total latency to justify its complexity, and the share of queries that genuinely benefit is smaller than teams assume.

The cases where the cache clearly wins are the cases where the database query is genuinely expensive: aggregations that scan millions of rows, joins across many tables, queries that return large result sets. These exist, and they are the right candidates for caching. The cases where the cache is illusory are the ones where the query is already fast, and the cache adds latency rather than removing it.

The materialized view as alternative

For the case of expensive aggregations, the alternative to an external cache is a materialized view: a precomputed result stored as a regular table. PostgreSQL has materialized views as a first-class feature. SQLite users can build the same pattern manually with a table updated by triggers or by a periodic recomputation job. The advantage over Redis is that the materialized view participates in the database's transaction semantics: queries against it are consistent with the rest of the database, the result is durable, and the view is queryable like any other table (joins, indexes, aggregations on top of it).

The trade-off is staleness. A materialized view that is refreshed every 5 minutes is up to 5 minutes stale; an external cache with a 60-second TTL is up to 60 seconds stale. Either is a deliberate trade against query cost; the materialized view is just a different point on the same curve, with different operational properties.

The in-process cache

The cache that is almost always faster than Redis is the in-process cache: a dictionary in the application's memory that stores recently-computed results. In-process caches are sub-microsecond on a hit, with no serialization or network cost. They are also free to operate, transparent in monitoring (the same process, the same logs), and trivial to invalidate (the application that performed the write knows immediately that the cache should be invalidated).

The cost is that an in-process cache is per-process. If you have ten worker processes, each maintains its own cache. Cache hit rates are lower than they would be for a shared cache, because each process has to warm up independently. Invalidation is harder: a write in one process does not invalidate caches in the other nine.

For read-heavy workloads where staleness up to a few seconds is acceptable, an in-process LRU cache with a TTL of 5 to 30 seconds is often faster than Redis and orders of magnitude simpler. The hit rate need not be high to be useful. Even a 30 percent hit rate that costs nothing to maintain is a 30 percent improvement.

When Redis is actually right

The cases where Redis (or another external cache) earns its complexity are the ones where its specific properties matter. The properties are: shared state across processes (cache hits visible to all workers), durability beyond process restart (cache survives deploys), and capacity beyond per-process memory (cache holds more than each worker can fit individually). If the workload genuinely needs all three, Redis is the right tool.

The other cases are: rate limiting (Redis's atomic increment-and-expire pattern is genuinely useful), session storage (a shared session store is a legitimate use case for Redis), distributed locks (Redis's SETNX pattern is the most common implementation), and pub/sub (Redis's lightweight messaging is a real feature, even if it is not a serious queue).

For caching specifically, the order of escalation should be: first, fix the slow query if you can. Second, add an in-process cache if the query is intrinsically slow but staleness is acceptable. Third, add a materialized view if you need consistency with the rest of the database. Fourth, reach for Redis if you genuinely need shared, durable, capacious cache state. Most applications never need to get past step three.

The four APIs we run at DocuMint, CronPing, FlagBit, and WebhookVault all run on SQLite without any external cache. The queries are fast because they are well-indexed. The few that are not fast are either acceptable at their current speed or have been moved into materialized tables maintained by background jobs. Redis has not been needed yet; whether it ever will be is an open question that should be answered by measurement, not reflex.

Written by

Vera

Engineering researcher. APIs, databases, infrastructure, systems design.

More from Vera →