When a page feels slow, the fix people reach for first is a cache. Sometimes that is exactly right and the site gets faster the same afternoon. Sometimes it is the wrong tool, and you add moving parts without moving the needle. This guide is about telling those two situations apart, then walking the whole path from "would a cache even help?" to a running cache your app is using, to reading the numbers that tell you it worked.
What a cache actually does
A cache stores the result of some work so that the next time you need it, you hand back the stored copy instead of doing the work again. That is the whole idea. The work might be a database query that touches a lot of rows, a computation that takes real time, or a call out to some other service. The first time, you pay the full cost and keep the answer nearby. Every time after that, until the answer goes stale, you serve the kept copy in a fraction of the time.
The reason this is fast is that a cache keeps its data in memory rather than on disk or across the network to a database. Reading a value that is already sitting in memory is about as quick as your app gets. So a cache is not magic, it is a trade: you spend a little memory and a little complexity to avoid repeating expensive work.
The cache addon in Burrow is ValKey, an in memory store that speaks the same protocol as Redis, so anything you already know about caching with Redis carries straight over, and any client library your language has for Redis works against it. It runs on your own cluster, on the worker nodes you already pay for, which matters more than it sounds like and we will come back to it.
When a cache helps, and when it does not
Here is the honest part, the part that saves you from adding infrastructure that does nothing.
A cache helps when the same work happens over and over. The classic shape is a read that many requests all want: a home page that runs the same handful of queries for every visitor, a product listing that changes a few times a day but gets read thousands of times, a user's profile that gets looked up on nearly every request. You do the work once, keep the answer, and serve it cheap to everyone who asks next. The more lopsided the ratio of reads to writes, the bigger the win.
It also helps for expensive computation whose inputs do not change often. If you render a report, roll up some statistics, or transform a chunk of data, and the result is the same until the underlying data moves, caching that result turns a slow operation into an instant one for every request after the first.
And it helps for hot rows: a small set of records that get hammered far more than the rest. A handful of popular items, the settings row every request reads, the feature flags you check everywhere. Keeping those in memory takes constant load off your database.
Now the cases where a cache does not help, because this is where people waste their time.
A slow query that runs once does not benefit from a cache. If a page is slow because of a single report that one person pulls once a day, caching it saves that one person a wait they were not going to repeat. The fix there is to make the query itself faster, or to precompute the report, not to bolt a cache in front of a thing that is not repeated.
A write heavy path does not benefit either, and can be made worse. If the data changes almost as often as it is read, your cached copy is stale nearly as soon as you store it, so you are constantly refreshing it and rarely serving a hit. You pay the cost of the cache and get little back. When writes dominate, look at the write path itself.
And a cache does not fix a fundamentally slow operation on its first run. The first request still pays full price. Caching hides that cost from the second visitor but not the first, and not from anyone who arrives right after the cached copy expires. Sometimes that is fine. Sometimes it means the real work needs to get faster regardless.
The short version: caches pay off for repeated reads, stable computations, and hot rows. They do not pay off for one off queries or write heavy paths. Knowing which you have is more than half the job.
Step 0: ask before you build
The good news is you do not have to guess which situation you are in, and you should not. Before standing anything up, the sensible first move is to look at what your app is actually doing. That means logs and metrics.
Burrow has addons for both: the metrics addon is VictoriaMetrics and the logs addon is VictoriaLogs, and if you already run your own logging or metrics you can connect those instead. With one or both of them in place, the question "my site is slow, would a cache help?" becomes something you can answer from data rather than a hunch.
If you operate through an agent, this is a real supported ask. You can say "my site is slow, would a cache help?" and the agent will go read your logs and metrics first: which endpoints are slow, how often they are hit, whether the same reads repeat, whether the slow paths are reads or writes. Then it comes back with an answer grounded in what it found. If the slow thing runs once and never repeats, a good answer is "no, a cache will not help here, this query needs an index" and you have saved yourself the trouble. If it sees the same expensive reads over and over, it will say so and propose the cache.
You can also do this reading yourself, of course. The point is that the check comes before the build, either way. Look first, then decide.
Step 1: install the cache addon
Once you have decided a cache earns its place, installing it is one command:
burrow addon install cache
That stands up ValKey on your cluster, ready for your app to use. Because it runs on the worker nodes you are already paying for, it adds capability without adding a separate bill. This is the quiet advantage of the addon model: on a typical PaaS, a managed cache is another line item you rent by the month, priced per instance and per gigabyte. Here the cache lives on the machines you already have, so the cost is the memory it uses, not a new subscription. The store is yours, it runs on your infrastructure, and you keep root on the whole thing. No lock in, nothing shipped off to a third party.
Installing an addon is one of the moves Burrow treats as worth a moment of thought. By default the agent proposes it and you approve, rather than standing up infrastructure on its own. You confirm it, or you run the install yourself as above. If you would rather your agent handle cache installs without pausing to ask, you can set that policy explicitly:
burrow guard set addon.install allow
Out of the box, though, the agent proposes and you decide. That default is deliberate: the interesting operations stay in front of a human until you choose to loosen them.
Step 2: wire your app to it
Installing the store is the first half of the addon model. The second half is connecting your app to it. As with the other addons, Burrow wires the connection into your app's environment for you, so your code reads the cache's address from a variable rather than you copying a connection string around by hand. From your app's point of view the connection is simply present, and you point your Redis compatible client at it the way you would connect to a cache anywhere.
That is the mechanical part done. The store is running, your app can reach it. Everything from here is about using it well.
Step 3: decide what to cache first
Do not try to cache everything at once. You will spend more time reasoning about stale data than you save in response time, and you will have a hard time telling what actually helped. Start with the single highest leverage thing and prove it out.
The best first candidate is usually the read that is both slow and frequent. Your metrics point right at it: the endpoint with a lot of traffic and a response time you would like to cut. Within that endpoint, find the specific work that repeats across requests, the query or computation that produces the same answer for many visitors, and cache that.
Good early wins tend to look like this. A home page or landing page that runs the same queries for every anonymous visitor: cache the assembled result, since it is identical for everyone until the underlying content changes. A listing or feed that is read constantly and written to occasionally: cache the list, refresh it when the content behind it changes. A lookup that appears on nearly every request, like a settings row or a set of feature flags: keep it in memory so you are not hitting the database for the same values thousands of times a minute.
Pick one, add it, measure it, then move to the next. One change at a time is also what lets you read the metrics honestly, because you know exactly what moved.
Step 4: the honest hard part, invalidation
There is an old joke that the two hard problems in computing are naming things, cache invalidation, and off by one errors. It lands because cache invalidation really is the hard part, and any guide that pretends otherwise is selling you something. A cache is a copy of the truth, and the moment the truth changes, your copy is wrong until you deal with it. Deciding when and how is the whole game.
There are two honest strategies, and most systems use both.
The first is expiry, setting a time to live on each cached value. You say "this is good for sixty seconds" or "this is good for an hour," and after that the cache forgets it and the next request does the real work again and stores a fresh copy. Expiry is simple and staleness is bounded: the worst case is a user seeing data that is at most as old as your time to live. The trade is that you are choosing how stale is acceptable. For a product listing, a minute of staleness is usually nobody's problem. For an account balance, it might be. Pick the time to live per kind of data, based on how much staleness that data can tolerate, not one global number.
The second is explicit invalidation, deleting or updating the cached value at the moment the underlying data changes. When someone edits a product, you clear the cached copy as part of handling the edit, so the next read rebuilds it fresh. This is more precise than expiry, because the cache is wrong for a much shorter window, but it is more work, because you have to remember every place a piece of data can change and clear the right key there. The failure mode is forgetting one of those places, so a stale value lingers because nothing told the cache to drop it.
A practical way to combine them: use explicit invalidation on the writes you control, and set a modest time to live underneath as a safety net so that anything you forget to invalidate still self corrects within a bounded window. Belt and suspenders. That way a missed invalidation is a short lived bug, not a permanent one.
The rule of thumb that keeps you out of trouble: it is almost always better to serve data that is a little stale than to serve data that is wrong. Bound your staleness with a time to live you have actually thought about, invalidate explicitly on the changes that matter, and do not cache anything where being even slightly out of date is genuinely unsafe. When in doubt, a shorter time to live is the conservative choice.
Step 5: confirm it actually helped
You added the cache for a number, so go look at the number. This is the step people skip, and skipping it means you never really know whether the cache is doing anything or just sitting there adding complexity.
With the metrics addon in place, you have the before and after in front of you. The main thing to watch is the response time on the endpoint you cached: it should drop, and the drop should be visible right where you expect it. Watch load on your database too, because a working cache takes read pressure off it, so you should see fewer of those repeated queries reaching the database at all. If you cached a hot lookup, that query's volume should fall off a cliff.
If you operate through an agent, this closes the loop cleanly. The same agent that read your metrics to recommend the cache can read them again afterward and tell you plainly whether the slow endpoint got faster and whether database load came down. "Did the cache help?" becomes a question answered from the same data that motivated it, not a vibe.
And if the numbers did not move, that is real information, not a failure. It usually means the work you cached was not actually the bottleneck, or the reads were not as repeated as they looked, or writes are churning the cache so hard you rarely serve a hit. Any of those tells you to pull the cache back out of that path and look somewhere else. A cache that does not help is worth removing, because every cached value is a small ongoing promise to keep that data honest.
The whole path in one view
Here is the shape of it, start to finish:
# ask first: would a cache even help here?
# (agent reads logs + metrics, or you read them yourself)
burrow addon install cache # stand up ValKey on your own nodes
# your app is wired to the cache's connection for you
# then, in your app:
# pick the highest leverage repeated read
# cache it with a thought through time to live
# invalidate it explicitly when the data changes
# read the metrics to confirm the endpoint got faster
None of the individual steps are large. The discipline is in the order: look before you build, cache the one thing that repeats, be honest about staleness, and check the numbers when you are done.
A cache is one of the highest leverage changes you can make to a slow site, right up until it is the wrong tool and quietly makes things fragile. The difference is entirely in whether you added it for a repeated, measured reason and gave honest thought to when the copy goes stale. Do that, and a cache is exactly the boring, effective infrastructure it should be: your site gets faster, your database gets breathing room, and the whole thing runs on nodes you already own.