DEV Community

Cover image for 10,000 Users. One Live Price. How Stale Is Too Stale?
Amirsaeed S Komjani
Amirsaeed S Komjani

Posted on

10,000 Users. One Live Price. How Stale Is Too Stale?

Imagine you are building a platform where users can buy and sell gold, silver, commodities, or crypto.

At first, everything looks fine. The system is responsive, the APIs are fast enough, and the database seems perfectly capable of handling the traffic. Then the number of users starts growing.

Now a few thousand people are looking at the same market at almost the same time. Prices need to stay fresh, balances need to remain accurate, orders should not be placed against outdated data, and charts are constantly refreshing in the background.

At this point, a very simple problem starts becoming expensive: the system keeps doing work it has already done.

The same price is requested again. The same calculation runs again. The same query reaches the database again. The same downstream service is called again, even though the answer may not have changed at all.

This is usually the moment when caching enters the conversation.

And very often the first suggestion is something like: “Let’s put Redis in front of it.”

That may help, but it does not really answer the important question.

The real question is: which data are we actually allowed to cache?

A live gold price for two seconds may be acceptable in one system and completely unacceptable in another. An order book may tolerate only a fraction of a second. A product description can probably remain stale for several minutes without anyone noticing.

But what about a user’s available balance?

What about available credit?

What about a price used to execute a financial transaction?

If a user completes a trade based on data that is only a few seconds old, are we still dealing with a performance issue, or have we created a business problem?

This is where I think caching becomes much more interesting than it first appears.

The same problem exists in large e-commerce platforms. Thousands of users may open the same product page, and most of what they need is identical: the title, description, images, category information, and product specifications.

There is no good reason to rebuild all of that from scratch for every request.

But stock, price, discount, and availability are different. They may be changing while users are looking at the page.

If you cache nothing, your infrastructure repeatedly performs work that could have been avoided.

If you cache everything, you may show a product as available when it is already sold out, display an expired discount, or return financial information that is no longer correct.

That is the point where caching stops being a simple performance optimization and becomes an architectural decision.

And the problem is much bigger than Redis.

A browser can cache. A CDN can cache. A reverse proxy can cache. The frontend can avoid duplicate requests and reuse already-fetched data. The application can cache expensive calculations, query results, or responses from external services.

Every layer gives you an opportunity to reduce unnecessary work.

But every layer can also create another version of your system’s state that may no longer match reality.

That trade-off is the part I think is often underestimated.

Writing Cache::remember()
 is easy.

Deciding what deserves to be cached, where that cache should live, how long the data may remain stale, and who is responsible for invalidating it is much harder.

TTL is not really an architecture either.

Setting a value to expire after 60 seconds sounds simple, but what happens if the underlying data changes one second after it was cached? Are we comfortable serving outdated information for another 59 seconds?

Sometimes the answer is yes.

Sometimes it absolutely is not.

And this decision usually has less to do with the caching technology itself and more to do with the meaning of the data.

A product catalogue may tolerate 30 seconds of stale information without any meaningful business impact. A dashboard may tolerate a few minutes. A live market price may tolerate only a few seconds.

A balance or credit limit may tolerate almost none.

Once you reach this point, the conversation is no longer only about response times.

It is about consistency, business semantics, and risk.

There is another problem that tends to appear only when traffic becomes large enough: cache misses can become a performance problem themselves.

Imagine that a heavily requested value expires while thousands of users are asking for it. Suddenly, a large number of requests may hit the database or pricing service at almost exactly the same time, all trying to rebuild the same cached value.

The cache that was supposed to protect your system can temporarily create a spike against the very service it was meant to protect.

This is where techniques such as request coalescing, background refresh, stale-while-revalidate, distributed locks, or jittered expiration times become useful.

But again, I do not think the important lesson is the list of techniques.

The important lesson is that caching introduces its own failure modes.

More caching does not automatically mean more scalability.

The same applies on the frontend.

Modern applications can easily generate unnecessary traffic through repeated fetching, polling, tab changes, component remounting, or multiple parts of the interface asking for the same resource independently.

It is very easy to solve one backend bottleneck while leaving the frontend constantly recreating it.

This is why I do not see caching as a backend-only concern.

It is a system-wide decision.

The browser, frontend, CDN, application layer, and database may all participate in the caching strategy, but they should not all make that decision independently.

At some point, someone needs to define what the source of truth is and how much distance from that truth each layer is allowed to have.

That is probably the biggest change in how I think about caching.

At the beginning of a developer’s career, caching often looks like a framework feature. Later, it becomes a performance tool.

But once you deal with real scale, the question changes.

It is no longer:

How should we cache this?

It becomes:

Which parts of our system’s reality are allowed to be outdated, and for how long?

And once you phrase the problem that way, many caching decisions suddenly become business decisions rather than purely technical ones.

A stale product description is probably harmless.

A stale balance may not be.

A stale market price might be acceptable for a chart but unacceptable for transaction execution.

The data can be identical, but the context in which it is used completely changes the risk.

This is also why I do not believe every early-stage product needs a sophisticated caching architecture from day one.

That would often be unnecessary complexity.

But there is a big difference between prematurely caching everything and understanding early which parts of the system may eventually require explicit freshness and consistency rules.

Waiting until the first serious performance crisis usually means these decisions are made under pressure.

And under pressure, the solution often becomes something like:

“Put it in Redis for 60 seconds and we’ll fix it later.”

Sometimes that works.

Sometimes it simply moves the problem from performance to consistency.

For me, the more interesting question is not whether a system should use caching.

It is when caching should stop being an implementation detail and become an explicit architectural decision.

I’m curious to hear from CTOs and engineers who have dealt with real scale:

At what point in a product’s growth should caching become an explicit architectural decision, rather than something added after the first performance crisis?

Top comments (0)