When to Use Redis as a Cache (and When It Makes Things Worse)
A cache is a second copy of your data that is allowed to be wrong. This is the decision frame: what to cache, what never to, the four failures that only appear in production, and why a cache full of sessions is a compliance object.
A cache is a second copy of your data, kept somewhere faster, that is allowed to be wrong. The last clause is the whole subject. Everything that goes well with a cache and everything that goes badly comes from the same property: there are now two copies of the truth, and nothing in the system guarantees they agree.
The question of when to use Redis as a cache is old enough that the results page answering it is visibly aging. As of writing, the top organic result in Germany is a thread on r/webdev from six years ago with ten comments in it, and the fifth is a Stack Overflow question roughly twelve years old. Most of the pages between them belong to Redis itself. They are good pages. A vendor's documentation is not where you find out when its product is the wrong choice.
So this stays at the level of the decision rather than the implementation. What earns a cache, what should never go in one, the four failures that only introduce themselves once real traffic arrives, and the part almost nobody writes down: a cache holding session tokens is a store of personal data, with the residency and processor obligations that follow from that.
Is Redis a cache or a database?
Both, and that is the root of most of the trouble. Google carries this question in the related searches of both results pages for this topic, in that exact wording, which suggests it is not only beginners asking it.
Redis has no mode switch. The same program serves as a throwaway cache and as a system of record, and which one you have is decided by three configuration choices you may never have made deliberately: whether keys carry an expiry, what happens when memory fills up, and whether anything is written to disk. Get the combination wrong in one direction and you have a database that quietly discards rows. Get it wrong in the other and you have a cache that stops accepting writes at the worst possible moment, which is a scenario further down this page.
Think of it as a data structure server that happens to live on the network, with sub-millisecond access to strings, hashes, lists, sets, sorted sets and streams. Caching is simply the most common thing people do with one.
The rule underneath everything below
A cache is a second copy of your data. Every problem in this article is some version of the two copies disagreeing, or of the second copy running out of room. If you keep that sentence in mind, most of the failure modes stop being surprising.
Before the cache: what is actually slow?
A cache is often the second-best answer to a problem that has a cheaper first answer. Adding one is easy enough that it gets reached for before anyone has looked at where the milliseconds go, and the result is an application with the same underlying problem plus an extra network hop, an extra dependency and an extra copy of the data to keep honest.
| What is actually slow | What a cache does about it | The cheaper move to try first |
|---|---|---|
| One query scanning a large table | Hides it behind a TTL until the next miss, when it is slow again | An index. The query plan names the column |
| Forty small queries inside one request | Turns forty database round trips into forty cache round trips | Fetch them in one query |
| Every request opening a fresh database connection | Nothing at all | Connection pooling |
| Connections exhausted under load | Nothing, and adds a second pool to size | Fix the pool, then look again |
| The first request after an idle period | Nothing. The cache is cold too | Keep the container warm |
| A response computed from data that changes hourly | Removes the work entirely | This one is the cache |
The two database rows get conflated constantly, and they are different problems. Running out of connections produces the too many clients already error and its own diagnosis path, and a cache does not help with it: your application still opens a connection on every miss, and misses arrive in bursts. The fix is connection pooling in front of PostgreSQL, which is a different layer solving a different problem. Do that first, then measure again, because the cache you were about to add may turn out to be unnecessary.
Slow first requests deserve the same scepticism. If the complaint is that the application is fine once it is warm, the problem is the container going to sleep and being rebuilt on demand, and a cold cache makes that first request slower rather than faster.
When to use Redis as a cache
Four shapes of workload reliably pay for the second copy. They share a property: the data is read far more often than it changes, or it is not really data at all but a counter or a token.
Repeated expensive reads is the classic case. A product catalogue, a pricing table, a rendered fragment of a page, an aggregate that takes two seconds to compute and is requested four hundred times an hour. What makes it worth doing is the ratio between reads and writes. The size of the object barely enters into it.
Sessions are the case that grows on you. As soon as an application runs on more than one instance, in-process session storage stops working, because the second request lands on a different container and the user appears logged out. Redis solves this by being outside all of them, and the expiry that makes people nervous elsewhere is exactly the behaviour a session wants.
Counters and limits are a fit for a different reason. Rate limits, view counts, feature-flag rollouts and queue depths are all small numbers updated concurrently, and Redis gives you atomic increments without a transaction against your primary database. Nobody would call this caching, and it lives on the same instance.
Then there is anything derived. If a value can be recomputed from data you still hold, losing it costs CPU rather than correctness, and that is the property that makes a cache safe to lose. Apply the test literally: if the instance were wiped right now, what breaks?
| Data | Cache it? | Why |
|---|---|---|
| Catalogue read on every page, updated nightly | Yes | Read-heavy, and an hour of staleness costs nothing |
| An aggregate that takes seconds to compute | Yes | Losing it costs CPU, not correctness |
| User sessions across several instances | Yes, with the TTL set to the session length | Shared by definition, and expiry is the feature |
| Rate-limit and quota counters | Yes | Ephemeral by nature, atomic increments |
| Permission and role checks | Carefully, with a short TTL | A stale permission is a security bug on a timer |
| Stock level or balance at checkout | No | Read it from the source that owns it |
| Anything you could not recompute | No | That is a database, and it needs to be treated as one |
When not to use Redis cache?
Four cases, and the first two account for most of the regret.
The data has to be right at the moment it is read
Balances, stock levels, permissions, anything a decision is made on. A cache is a window onto the past whose width you chose when you picked the TTL, and 300 seconds of stale permissions is a security bug with a five-minute fuse. If the answer to "what if this value is thirty seconds old" is anything other than "nothing much", the value does not belong behind a cache without an explicit invalidation path.
Writes outnumber reads
A cache pays for itself across repeated reads of the same key. Data written frequently and read once or twice gives you the write cost, the invalidation cost and the memory cost, against almost no benefit. Changing the write pattern does not rescue this, and the four patterns and what each one costs on the write path are worth reading before deciding otherwise. The pattern to watch for is a key that is updated on every request and read on the same request.
One instance, and the data fits in memory
This one is unpopular and keeps being true. If your application runs as a single process and the working set fits in its own memory, a plain in-process map is faster than Redis, because it does not cross a socket. The r/dotnet thread that Google cites in its AI Overview for this topic reaches the same conclusion from experience rather than theory. Redis starts winning the moment there is a second instance, or the moment the cache needs to survive a deploy.
Nobody has measured yet
Adding a cache before you know which call is slow buys you a dependency, a second copy of the data and a new failure mode, in exchange for a guess. The table earlier in this article exists because four of its six rows have a cheaper answer than caching, and you cannot tell which row you are in without looking.
Four ways a cache fails in production
None of these show up in development. All four show up on the day traffic arrives, and three of them announce themselves as a failure in the application rather than in the cache, which is what makes them slow to diagnose.
maxmemory-policy noeviction
The expectation almost everyone brings to a cache is that a full cache throws away old entries. Redis does that only if you tell it to. In the upstream redis.conf, as of writing, the default eviction policy is noeviction: once memory is exhausted the server stops accepting writes and returns an error to any command that would use more of it. Reads keep working, so the application looks half alive.
127.0.0.1:6379> SET session:9f21c4a7 "{...}"
(error) OOM command not allowed when used memory > 'maxmemory'.
127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
127.0.0.1:6379> INFO memory
used_memory_human:256.00M
maxmemory_human:256.00M
127.0.0.1:6379> INFO stats
evicted_keys:0 # nothing was ever evicted, and that is the tellThe line to read is evicted_keys:0. A cache that has never evicted a key while sitting at its memory ceiling is not a cache, it is a full disk with a network interface. Managed providers and distribution packages frequently override the upstream default, so check the running value rather than assuming either way. Which policy to switch to, and why the choice between recency and frequency matters more than it looks, is its own subject.
MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk.
The second failure is the mirror of the first: persistence nobody asked for. Redis ships with RDB snapshotting enabled, and with stop-writes-on-bgsave-error set to yes, so a failed background save makes the server refuse writes until the problem is fixed. The usual cause is the disk filling, and the usual reaction is confusion, because the team believed they were running a cache with nothing on disk at all.
127.0.0.1:6379> SET user:1042:profile "{...}"
(error) MISCONF Redis is configured to save RDB snapshots, but is currently
not able to persist on disk. Commands that may modify the data set are
disabled, because this instance is configured to report errors during
writes if RDB snapshotting fails (stop-writes-on-bgsave-error option).
Please check the Redis logs for details about the RDB error.
127.0.0.1:6379> CONFIG GET stop-writes-on-bgsave-error
1) "stop-writes-on-bgsave-error"
2) "yes"
127.0.0.1:6379> INFO persistence
rdb_last_bgsave_status:err
rdb_changes_since_last_save:184203Two decisions come out of this. Decide explicitly whether this instance is durable or disposable, and configure it to match rather than inheriting whatever the image shipped with. And if it is durable, then it is not a cache and the rest of this article applies to it only loosely, because losing it now costs data rather than CPU.
The stampede when everything expires at once
Set a thousand keys with a 3600-second TTL during a deploy and they expire within milliseconds of each other an hour later. Every request that would have been a hit becomes a miss, every miss queries the database, and the database receives an hour of accumulated traffic in one second. The application was fine at 14:00 and fell over at 15:00 with no change deployed in between. Explaining that afterwards is its own kind of difficult.
Search interest in the term has roughly tripled over the past year, as of writing, and more teams appear to be meeting it. The mitigations are unexciting: add jitter so TTLs land in a range rather than on a point, let a single request recompute a value while the others serve the stale copy, or refresh hot keys in the background before they expire. Jitter alone removes most of the risk and costs one line.
// Every key expires at the same instant. This is the bug.
await redis.set(key, JSON.stringify(value), 'EX', 3600);
// Spread expiry across a ten-minute window instead.
const ttl = 3600 + Math.floor(Math.random() * 600);
await redis.set(key, JSON.stringify(value), 'EX', ttl);Keys that outlive the schema
The fourth failure arrives with a deploy. You add a field to a serialised object, ship it, and for the next hour the application reads cached entries written by the previous version, which do not have that field. Depending on the language this surfaces as a null, a type error, or a page rendering with something missing and no error at all.
This is one of two invalidation decisions worth making deliberately; the other is whether a write deletes the cached key or overwrites it, which is where most stale-cache bugs actually come from. The remedy here is to put a version in the key rather than to remember to flush. A prefix such as v3:user:1042 means a release that changes the shape of the object also changes the namespace, old entries are ignored and then expire on their own, and a rollback finds its own entries still valid. Flushing the whole instance on deploy is the alternative, and it trades a correctness bug for a stampede.
Your cache holds personal data
Across both results pages measured for this article, eighteen organic results and one fully expanded AI Overview, not one mentions where the cache runs, who operates it, or what is legally inside it. The answer is less comfortable than the silence suggests.
A cache holding sessions holds session tokens, user identifiers, email addresses, roles and whatever else was convenient to keep on the session object. Under the GDPR that is personal data, and it does not become less so for being temporary or being called a cache. The same residency questions that apply to your database apply here: which region the instance runs in, whether snapshots or replicas land somewhere else, and whether the connection between your application and the cache is encrypted. Check the last one today. The difference between an encrypted session store and an unencrypted one is a single character in a URL.
# Plaintext. Sessions cross the network in the clear.
REDIS_URL=redis://default:password@cache.example.eu:6379
# TLS. One extra 's', and the port is usually the same 6379.
REDIS_URL=rediss://default:password@cache.example.eu:6379The cache belongs in the processor list
Whoever runs your Redis instance is processing personal data on your behalf and needs a data processing agreement like any other subprocessor. Caches get missed in compliance reviews more than any other component, because the team classifies them as infrastructure rather than storage.
What a cache costs
"Is Redis cache free" sits in the related searches for this topic with nobody in the top ten answering it, so here is the shape of the answer. Redis itself is free software you can run on a server you already pay for. A managed instance is priced by memory, and memory is the only dimension that matters until you reach failover and persistence.
Free tiers exist across European providers and they are small and time-limited, which is appropriate: a free cache is for proving the hypothesis, not for holding your sessions in production. The number to watch is not the monthly price but the clock, since a free database tier that expires after a fixed period is a different product from a free application tier that runs indefinitely.
The real cost question is sizing, and it is answered by the first failure in this article rather than by a pricing page. An undersized cache with noeviction set is an outage, and an undersized cache with eviction enabled is a hit rate falling quietly until the database is doing the work again. Both cost more than the next tier up. Start from the working set — how many keys, how large, and how long they need to live — and price that, rather than starting from the cheapest line and hoping.
If you are pricing a managed cache against running one yourself on a VPS, the arithmetic is the same one that applies to a €5 server compared with a managed platform: the advertised figure is not the invoice, and the maintenance is not free just because it is unbilled.
How Runsite handles it
Managed Redis for caches, sessions and job queues runs on Runsite as a toggle next to your application rather than as a separate account with a separate vendor. It is Redis 7+ with TLS on by default, as of writing, so the rediss:// connection string above is the one you are handed rather than something you have to opt into.
| Plan | Price | Memory | Persistence |
|---|---|---|---|
| Free | €0 | 50 MB, shared | None. Runs 30 days, then upgrade |
| Starter | €5/mo | 256 MB | RDB, TLS, private networking |
| Standard | €13/mo | 1 GB | RDB + AOF, automatic failover, metrics |
| Plus | €25/mo | 2 GB | RDB + AOF, automatic failover |
On the residency question from earlier: instances run in the Frankfurt, Germany region, and the keys, the sessions and any RDB or AOF files stay inside the EU. A signed GDPR data processing agreement is attached to every plan including the free one. The cache ends up in the same processor arrangement as the rest of the stack, rather than staying the component nobody wrote down. Most teams run it beside managed PostgreSQL hosted in the EU from €5 a month, which is the pairing the first half of this article assumes.
The free 50 MB instance is the honest use of a free tier: enough to hold a few thousand cached objects and find out whether the hit rate justifies the second copy, before anyone pays for it. Configuration details, including the eviction policy on your instance, are in the Runsite docs.
The short version
- A cache is a second copy of your data that is allowed to be wrong. Every failure below is that sentence playing out.
- Redis is a cache or a database depending on three settings you may not have chosen: expiry, eviction policy and persistence. There is no mode switch.
- Measure before caching. Of the six common slow-application symptoms in the table above, four have an answer that is cheaper than a cache, and two of those are an index and a connection pool.
- Cache repeated expensive reads, sessions shared across instances, counters and limits, and anything derived that can be recomputed. Losing it should cost CPU, not correctness.
- Do not cache values that must be right when read. A permission check behind a 300-second TTL is a security bug with a five-minute fuse.
- A single instance whose working set fits in process memory is faster without Redis. Redis wins at the second instance, or when the cache must survive a deploy.
maxmemory-policydefaults tonoevictionupstream, as of writing. A full instance then returnsOOM command not allowed when used memory > 'maxmemory'.and refuses writes while reads keep working. Checkevicted_keysagainst your memory ceiling.- RDB snapshotting is on by default and
stop-writes-on-bgsave-errorisyes, so a full disk producesMISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk.Decide durable or disposable, and configure it deliberately. - Identical TTLs set during a deploy expire together and send an hour of traffic to the database in one second. Add jitter; it costs one line.
- Version your keys with a prefix so a schema change changes the namespace. Flushing on deploy trades a correctness bug for a stampede.
- A cache holding sessions holds personal data. It needs the same residency answer and the same processor agreement as your database, and it is the component most often missed in a compliance review.
- Price the working set, not the cheapest tier. An undersized cache is either an outage or a hit rate quietly falling until the database is doing the work again.