Most teams never choose a cache store. The framework ships with a default, or the last project used Redis, so that is what goes in.
The choice shows up later. The app scales to a second instance and the cache stops agreeing with itself. A deploy wipes it. It fills up and starts evicting. None of this throws an error. Pages just get slower, and nobody suspects the thing that was meant to make them fast.
For most web apps the answer is a managed Redis-compatible store, with a short-lived in-process layer in front of it, and code that treats a cache failure as a miss. The rest of this rule is how to tell when that answer is wrong for you.
Before you pick a store, check whether you need a data cache at all. If whole pages can be static or cached at the edge, that is cheaper than any store. See Do you choose the right rendering strategy for your website? and Do you use Output Caching for better API performance?. If the data comes from a headless CMS that bills by the API call, see Do you cache headless CMS content to stay inside your API quota?.
Cache: Redis, Basic tier for now. We can resize it later.
❌ Figure: Bad example - The store is named, but nothing says what happens when the app scales, the cache fills or the cache fails
Cache: Azure Managed Redis, one instance per environment, with high availability on in production. Every entry has an expiry, and an alert fires well before memory is full. An in-process layer holds the navigation for 30 seconds. Publishing clears the shared store by tag. A cache failure is logged and treated as a miss.
✅ Figure: Good example - Each of the 7 questions has an answer the team can check in production
| Store | Shared across instances | Survives an app deploy | Network call per read | Good for |
|---|---|---|---|---|
| In-memory, per process | No | No | No | One instance, or the fast layer of a two-layer cache |
| File on local disk | No | Only on a persistent disk | No | One server, or a page cache the web server serves directly |
| Database | Yes | Yes | Yes | Small apps with light cache traffic |
| Redis, Valkey or Garnet | Yes | Yes | Yes | The default shared cache |
| Memcached | Yes | Yes | Yes | Simple key-value caching |
| Two layers | Yes, through the shared layer | The shared layer does | Only on a local miss | High read volume across several instances |
Tag or group invalidation is not in the table, because it depends on your framework's driver rather than the store. Laravel cannot tag entries on its file or database drivers. .NET's HybridCache tracks tags itself, so it supports them over any store.
The fastest cache is in your own process, with no network call and nothing extra to host. It is the right choice while you run one instance. Once you scale out, each instance holds its own copy, a publish clears one of them, and the rest keep serving stale content.
In a typical PHP app each request starts fresh, so an in-memory driver like Laravel's array forgets everything when the request ends. And watch the names: ASP.NET Core's distributed memory cache is not actually distributed.
A shared store runs on its own server. Every instance reads and writes the same copy, so there is one place to invalidate and the cache survives your app's deploys. Redis is the usual choice, and ASP.NET Core's output cache can use it as its store.
Redis moved from BSD to source-available licences in 2024, and Redis 8 added AGPL as an option in 2025. See Redis licences, and check which one applies if you host Redis yourself. The alternatives:
A managed service gives you patching, replication, failover, TLS and metrics. A Redis container next to your app is cheap, but you own all of that, and it goes down with the host.
On Azure, use Azure Managed Redis. Azure Cache for Redis is being retired, and new customers can no longer create it. Azure Managed Redis lets you turn off high availability to save money, which is fine for dev and test but means downtime and data loss in production.
Hosting it yourself is the right call for local development and tests. .NET Aspire and Testcontainers start one for you, so local and CI runs use the same kind of store as production.
Whichever you choose, give each environment its own instance. A shared one lets staging evict production's keys. Keep sessions and other data you cannot lose off the cache instance too, because Redis suggests separate instances for cache data and persistent keys.
Weigh the cost against what the cache saves. Price one instance per environment on the Azure Managed Redis pricing page, then compare it with the database load, API calls and app instances the cache removes. The cheapest tier is often the one that fails under real traffic, as the example below shows.
Measure memory with the cache on, under real traffic, until it levels off. Then alert well before it is full, as Microsoft's memory management guidance recommends.
Check the eviction policy. Azure Managed Redis defaults to volatile-lru, which only evicts keys that have an expiry. If your entries have none, a full cache stops accepting writes and returns errors. Give every entry an expiry, or use allkeys-lru on an instance that only holds cache data.
A shared store costs a network round trip per operation. One is cheap. Two thousand on one page are not. See Redis pipelining for why the round trip, not the server, sets the cost.
Two layers give you the best of both. Most reads come from the instance's own memory. A cold instance or an evicted entry falls through to the shared store instead of to the database.
In .NET, use HybridCache. It keeps an in-process layer in front of any IDistributedCache, such as Redis, SQL Server or Postgres. It also makes concurrent callers for the same key wait for one fetch, so a cold key costs one database call per instance rather than one per request.
Keep the local layer's expiry short. When HybridCache invalidates an entry, it clears the shared store and the current server's memory, but not the memory of your other servers. They keep serving their copy until it expires.
A file cache is fine on one server with a persistent disk. On containers, each instance has its own disk, and a deploy or restart usually wipes it. A self-hosted Next.js app keeps its cache on each instance's local disk by default, and revalidating on one instance leaves the others serving stale pages. The fix is a custom cache handler backed by shared storage such as Redis. See Do you know how to use NextJS caching system?.
A database cache puts the load back on the database it was meant to protect. Microsoft notes that a SQL Server cache in the same database as your app data can reduce performance, and recommends a dedicated instance. Laravel's default cache store is the database, so check what your app is actually using.
The worst trap is code that assumes a feature the store lacks. Laravel's Cache::tags() is not supported on the file or database drivers, and throws when you call it. Code written against Redis works until someone points it at another store.
A cache should never be the reason the app is down.
A Laravel marketing site on Azure rendered its pages from a headless CMS. Alongside a page cache, it kept CMS responses in Redis, so a page that missed the page cache could still render without calling the CMS.
Turned on in production, the Redis cache made the site 20 to 60 times slower, and it was turned off the same day. A second attempt with telemetry showed it worked once warm:
It never got warm, for two reasons.
The store was too small and shared. It was the smallest Azure Cache for Redis size: 250MB, one node, no SLA. Staging and production shared it. With the cache on, it reached 90% of capacity in 15 minutes and started evicting. The miss rate then stuck at 14%, because every eviction caused a rebuild that wrote more data.
Bookkeeping cost too many round trips. To support invalidation, the cache read a stored list of keys for every entry and asset in a CMS response, appended one key and wrote the whole list back. A render that filled the cache cost 2,013 Redis operations and 3.7 seconds of Redis time. The team first suspected the Redis client and swapped it. Each operation measured about 1.83ms, an ordinary round trip over TLS, so the client was never the problem. The number of round trips was.
The content cache stays off until both are fixed: a bigger instance for each environment, and a Redis set that appends a key in one command without reading anything back.
The same site also had a near miss. One blank cache setting would have switched it to the file driver, and its redirect middleware used tags, so every request would have returned a 500. It was found in time, and a broken cache now falls back to a database lookup.