How to Set Up HTTP Caching for APIs: ETag, Last-Modified, Vary

Traffic spikes and slow API responses hurt conversion and customer retention. We provide turnkey HTTP caching setup: we analyze the causes of failures, design caching modes, and implement them so that repeated requests are served instantly. As a result, response time is reduced several times over, and infrastructure stops growing along with traffic.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Frequently Asked Questions

HTTP caching for APIs: where it breaks and what the customer loses

HTTP caching is a layer that sits between the client and the backend and decides whether an already prepared response can be served without bringing up the application.

When it is absent, every request from a mobile client, every interface re-render, and every repeated call from an integration goes to the database and recalculates the same result.

On an average API, this turns into growing API latency: response time does not grow linearly but in steps, because the request queue begins waiting for database connections to be released.

Then the domino effect kicks in. Backend load grows with the number of clients, not with the volume of genuinely new data — a typical picture when 80% of traffic hits the same reference data, catalogs, and lists.

Under peak load, the resource headroom runs out before the main traffic arrives, and failures during peaks begin: first isolated timeouts, then mass errors on the "hottest" endpoints, which are precisely the ones called most often.

API caching removes this load at the protocol level rather than by adding servers.

The ETag and Last-Modified headers let the client ask "has it changed" and get a short response instead of the full body, Vary separates the cache by the necessary request attributes, and correct freshness directives determine how long a response lives without hitting the application.

As a result, repeated requests are closed at the boundary, and the backend deals only with new data.

Let's break down exactly where HTTP caching breaks in real APIs and how to configure it so that latency and failures stop being a bottleneck.

What managed caching delivers: measurable effects

HTTP caching stops being "tuning by eye" when every response has a validator: an ETag or Last-Modified.

The client sends If-None-Match / If-Modified-Since, the server responds with 304 Not Modified without a body — API response time on repeated requests drops severalfold, because only the header travels over the network, not the payload.

The backend doesn't rebuild the response either: the offloading comes not from client-side caching, but from the fact that the heavy logic never runs at all.

The Vary header solves the second half of the problem — correctly partitioning the cache by Accept-Encoding, Authorization, and language.

Without it, a proxy serves one client a response assembled for another: someone else's permissions, the wrong locale, compression for the wrong format.

This is a distinct class of failure — not a service outage, but a silent serving of incorrect data that gets caught in production. Managed caching eliminates it at the contract level, not with patches in the code.

Stability under load is the consequence. When the share of repeated requests closed by validation grows, peak backend load stops scaling with the number of clients: the same servers handle more traffic, and response time stays predictable.

Let's break down how this is configured in practice — from choosing a validator to the policy for proxies and CDNs.

Caching approaches: comparing modes and limitations

Caching is not "set it and forget it," but choosing a mode for the traffic profile. Below are the real alternatives that have to be chosen between when designing an API, with their applicability and limitations.

Separately, one should look at who manages the policy: the application, the reverse proxy, or the client.

Option When it fits Limitations
No caching Personal data, payment operations, responses that depend on user permissions Every request goes to the origin, load and latency increase
Cache-Control: no-store Sensitive responses that must not be left on disk Provides no speed benefit, only protects the data
max-age + public Public reference data, catalogs, rarely changing content Data staleness within the TTL, a reset is needed on update
ETag + If-None-Match Resources that change infrequently but must be fresh An extra round-trip for validation, a 304 response instead of a body
Last-Modified + If-Modified-Since Files, static assets, documents with a modification date Accuracy down to the second, false positives on re-saving
Vary by headers Content depends on language, authorization, device type Splits the cache into many variants, storage volume grows
Managed policies on the proxy Uniform rules for dozens of services Require coordination with the application, otherwise header conflicts

Combining modes is almost always necessary: ETag for dynamic content, max-age for static content, Vary where the response depends on the request context.

The key risk is a mismatch of headers between the application and the proxy: the client receives a stale response or, conversely, the cache does not work at all.

The choice of approach is fixed in managed policies at the gateway level, not in the code of each service. This way the rules remain predictable, and caching limitations are visible before release, not after an incident.

How we implement caching: stages of engineering work

API caching is not "turning on a plugin" but a sequence of decisions: where the cache sits, by which key it distinguishes responses, and at what point data is considered stale.

Below is how we carry out this work so that after launch the behavior is predictable rather than "seems to work."

  1. Header audit. We capture real API responses and look at what the server currently returns: whether there is a Cache-Control with a meaningful max-age, whether ETag or Last-Modified is set, and whether Vary on authorization and language headers is lost. It often turns out that some responses are cached where they shouldn't be, while others are, conversely, not cached at all.
  2. Policy design. For each resource type we define the mode: public reference data — a long max-age with stale-while-revalidate, personal data — private without a shared cache, mutable entities — validation via ETag and a 304 response. Separately, we fix the cache key and the set of fields in Vary.
  3. Cache configuration. We set the headers at the application and intermediary layer level, synchronizing them with each other so that the proxy does not overwrite the application's policy. We configure correct handling of conditional requests If-None-Match and If-Modified-Since.
  4. Invalidation verification. We run through data change scenarios: updating a record, changing access rights, publishing a new version. We make sure that after a write, the client and the intermediary cache receive fresh data rather than serving stale data.
  5. Load testing. We look at the share of 304 responses and cache hits, and the response time before and after. The goal is an increase in the share of validated responses and a reduction in load on the origin.
  6. Documentation handover. We provide a map of policies by endpoint, invalidation rules, and instructions for developers: which headers can be changed and which will break the cache.
  7. Ongoing support. After launch, we monitor hit metrics and adjust max-age where data changes more often than expected.

Describe your task — we will get back to you with an estimate and a work plan.

What the client receives: configurations and policies

Working with HTTP caching does not end with "turned on the headers and forgot about it," but with a set of artifacts that remain in the repository and outlive the contractor.

Each item below is something that can be opened, verified, and handed over to a new engineer without verbal explanations.

  • Cache configurations — working files for the reverse proxy and the application: rules for Cache-Control (max-age, s-maxage, stale-while-revalidate), generation of ETag and Last-Modified, configuration of Vary for Accept-Encoding and authorization. With comments explaining why exactly this set of directives was chosen for each response type.
  • Flow diagrams — diagrams of the request path: client → CDN → reverse proxy → application → storage. The diagram marks which layer caches the response, where revalidation happens, and which layer is responsible for invalidation.
  • Invalidation policies — the purge policy: by resource tags, by URL prefixes, by data change events. Indicating what is purged forcibly and what lives until max-age expires, and how this relates to business logic.
  • API documentation — description of endpoints indicating cacheability: which responses are public, which are private, where authorization is required and why such a response must not end up in a shared cache.
  • Verification policies — acceptance checklist: checking response headers, behavior on 304 Not Modified, correctness of Vary with different Accept-Encoding, absence of private data leakage through a shared cache.
  • Monitoring and alerts — metrics for cache hit ratio, response time on a miss, and revalidation. Thresholds at which an engineer receives a notification rather than learning about the problem from users.
  • Rollback instructions — the procedure to follow if a new caching policy breaks the output: how to quickly purge the cache at all layers and restore the previous configuration.

The package is self-contained: it can be used to audit current caching, reproduce the settings in a new environment, and hand over maintenance to another team. Describe your task — we will get back to you with an estimate and a work plan.

Case Study: How Managed Caching Eliminated Failure Spikes

The project is a marketplace with a catalog of several hundred thousand items and a mobile app that hit the API for every screen. The backend wasn't returning cache headers: Cache-Control wasn't set, ETag wasn't computed, and responses went out with no-store by default. Every app launch and every catalog navigation hit the database directly.

What It Was Like Before Implementation

At peaks — morning traffic and sales — catalog response time exceeded one second, the share of server error responses hovered around a few percent, and the database hit its connection ceiling.

Failures came in waves: overload caused timeouts, clients retried requests, and the load doubled out of nowhere. The on-call shift put out these fires with manual pool scaling and restarts.

What Was Changed

Managed caching was configured at the API gateway and application level. For reference data and product cards, ETag and Last-Modified were enabled — a repeat request with no changes returns 304 Not Modified and never reaches the backend.

For lists that depend on filters, roles, and language, Vary was set on the relevant headers to avoid serving the wrong version of a response.

Public data received Cache-Control with separate max-age and stale-while-revalidate directives: the client serves a stale copy while a fresh one is fetched in the background. Invalidation was tied to product change events rather than a timer.

What the Result Was

The share of 304 responses on repeat catalog requests exceeded half — these requests stopped reaching the database. Peak response time dropped severalfold, the share of server errors fell to fractions of a percent, and backend load during peak hours decreased by roughly half.

Wave-like failures stopped: the system handles sale traffic without manual intervention from the on-call engineer.

The implementation practice showed the main thing: the win comes not from the header itself, but from a coherent policy — what gets cached, for how long, by what criteria it's differentiated, and when it's invalidated.

We apply this approach in other projects on FastAPI and similar stacks, where predictability under load matters.

How is ETag different from Last-Modified, and when is Vary needed?

ETag and Last-Modified solve the same problem — giving the client a cheap way to check whether a resource has changed without transmitting its body in full.

Last-Modified relies on the time of the last edit and works with one-second precision: if two changes happened within the same second, or the file was overwritten with the same content, the tag lies.

ETag is a fingerprint of a specific version of the representation, so it catches changes that time cannot see, and works correctly when serving from a CDN and when assembling a response from multiple sources.

In practice, they are not set against each other but used together: ETag provides precise validation, while Last-Modified remains a fallback validator for intermediaries that cannot work with a fingerprint.

The client sends a conditional request with If-None-Match and If-Modified-Since, the server compares them and returns either the body or a short "not modified" response — saving traffic and response time by an order of magnitude, not by a few percent.

A weak ETag differs from a strong one: the former confirms semantic equivalence, the latter byte-for-byte equivalence. For a JSON API, a weak one is almost always sufficient; for static assets and file serving, a strong one is needed.

A beginner's mistake is generating an ETag from the process start time: after a restart or a move to another node, the tag changes, and the entire client cache is invalidated at once.

Vary answers a different question — not "is this the same version" but "is this the same client." If the response depends on the Accept, Accept-Encoding, Accept-Language, or authorization header, then without Vary an intermediate cache will serve the wrong version: a compressed response to a client that does not support compression, English to someone who asked for Russian.

Vary: Accept-Encoding and Vary: Accept-Language close this gap, but each added header fragments the cache into a larger number of variants, so you should list only what actually affects the response body.

Vary: Authorization and private responses deserve separate mention: they are either marked Cache-Control: private or forbidden from caching altogether, otherwise personal data will end up in a shared cache.

It is convenient to check the setup via logs: the share of "not modified" responses to conditional requests shows whether validation is working, and the cache hit rate for each Vary variant shows whether the storage is growing out of control.

Common technical concerns when setting up caching

Caching is usually already deployed — on the CDN, reverse proxy, or in the application itself, and the question is not "whether to enable it" but "how not to break what works." We start with an inventory: which routes are cached, by which keys, who sets the headers — the proxy or the backend.

Then we change things in a targeted way: first responses without personalization, then everything else, with verification at each step.

Authorization and caching conflict where the response depends on the user but the cache key depends only on the URL. The solution is either an explicit Cache-Control: private for such routes, or Vary: Authorization, or splitting into public and private perimeters.

We do not rely on "it won't be cached by default": we check every route where the response contains user data.

Verifying invalidation is a separate stage, not a formality. We look at what happens when a resource is updated: does the ETag change, does a 304 come back on a conditional request, does the old response leave the proxy after PURGE. If invalidation does not work, the client gets stale data and does not understand why.

We already have caching deployed — will we have to rebuild everything?

No. First we capture the current behavior: which headers the backend returns, what the proxy overwrites, where the keys diverge. A rebuild is only needed where the rules conflict — usually that is 2-3 routes out of dozens.

How do we avoid breaking authorization when enabling caching?

We separate the perimeters: public responses are cached by URL, private ones are marked private or get Vary: Authorization. After enabling, we run scenarios with different roles and check that a user does not receive someone else's response.

How do we make sure invalidation works?

We check the chain: the resource was updated — did the ETag or Last-Modified change, did the server return 304 on a conditional request, did the old response leave the proxy. If at least one step does not work, the cache serves stale content.

These concerns come up on almost every project — and are almost always solved with targeted configuration rather than a rewrite. Describe your configuration — we will get back to you with an estimate and a verification plan.

Let's discuss your API caching task

To come back with an estimate and an implementation plan, we need context about your API: specifically where caching doesn't work or doesn't work as expected. Describe the task in free form — that's enough for our engineer to understand the picture and propose a realistic plan.

What's useful to include with the description:

  • current API response headers — Cache-Control, ETag, Last-Modified, Vary: it shows what's already being sent to clients and where the logic breaks down;
  • architecture: monolith or multiple services, whether there's a CDN or reverse proxy in front of the application;
  • which methods and endpoints are cached, and which must stay strictly fresh (payments, balances, order statuses);
  • how authorization is set up — tokens in headers, cookies, sessions: this determines the correct configuration of Vary and cache privacy;
  • user complaints or metrics: stale data, redundant requests, growing load on the database;
  • constraints: regulations, security requirements, deadlines that cannot be moved.

Based on these inputs, we'll come back with a work estimate and an implementation plan: which headers and rules we set, how we configure the separation of public and private cache, how we verify correctness through conditional requests and rollback.

If forks in the road come up during the analysis, we'll discuss them with your engineer so that the solution fits into the existing infrastructure rather than requiring it to be rebuilt.

Describe the task — we'll come back with an estimate and a plan.

Our competencies:

Latest works

  • B2B ADVANCE company website development
    B2B ADVANCE company website development
    1542
  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1393
  • Website development for BELFINGROUP
    Website development for BELFINGROUP
    1081
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1368
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1139
  • Website development for FIXPER company
    Website development for FIXPER company
    1060