Skip to content
APIs

7 JSON API Caching Mistakes That Serve Stale or Private Data

·9 min read
7 JSON API Caching Mistakes That Serve Stale or Private Data - APIs guide from JSON Prettifier

Quick answer: Safe JSON API caching requires an endpoint-specific storage policy, correct Cache-Control semantics, complete cache-key variation, representation-aware validators, deliberate invalidation, explicit error caching, and end-to-end tests through the real proxy or CDN. A fast cached response is a failure when it is stale, belongs to another user, or represents different request options.

Caching bugs are convincing because they return valid JSON quickly. The problem appears later: one tenant sees another tenant's profile, a new record remains missing, or a client receives the English representation after requesting French. These seven mistakes turn cache behavior into an explicit API contract.

For the surrounding response, authorization, and versioning rules, start with JSON in REST APIs.

Compare two sample response bodies with JSON Compare. Cache behavior also depends on HTTP headers and cache configuration, which JSON comparison cannot validate.

1. Can a shared cache store personalized JSON?

Treat responses selected by credentials, cookies, tenant, role, or permissions as private unless you can prove they are identical for every authorized caller sharing the cache key. Use Cache-Control: private for browser-only storage or no-store when storage is unacceptable, and test the policy at the CDN boundary.

This header is dangerous on a user profile:

Cache-Control: public, max-age=300

If the shared cache keys only on URL, /api/me from Alice can be served to Bob. The JSON remains perfectly valid; the authorization boundary failed outside application code.

MDN's current Cache-Control reference recommends private for personalized responses and explains that public can make responses to authorized requests eligible for shared storage. That power should be deliberate, not copied from a performance checklist.

Do not rely on the CDN supposedly “ignoring” authenticated traffic. Configuration changes, origin shields, framework defaults, and custom cache rules can alter behavior. Send explicit response directives and test with two accounts, distinct tenants, and sequential requests through the real public hostname.

Cache keys should never include raw credentials in logs. If authorized shared caching is genuinely required, derive a safe partition such as an immutable tenant or permission variant controlled by trusted edge logic, and verify it cannot be supplied by an attacker.

2. Do you use no-cache when you mean no-store?

no-cache does not prohibit storage; it requires a cache to validate the stored response before reuse. no-store tells caches not to store it. Choose based on whether retention itself is allowed. For frequently checked but non-sensitive JSON, no-cache plus a validator can save bandwidth while keeping each reuse validated.

These policies are not synonyms:

Cache-Control: no-cache, private
ETag: "customer-42-v17"

The browser may store the response, but it must validate before reuse. By contrast:

Cache-Control: no-store

asks all caches not to store the response. Use it for highly sensitive payloads or flows where persistence is unacceptable under the application threat model.

Avoid cargo-cult bundles such as private, no-cache, no-store, max-age=0, must-revalidate. Contradictory-looking directives make intent difficult to audit, and the most restrictive directive may make the rest meaningless. State the minimum clear policy that matches the endpoint.

Also distinguish freshness from invalidation. max-age=60 says a stored representation can be reused for a minute under normal rules; it does not promise every intermediary will purge immediately when your database changes. If a minute of staleness is harmful, choose validation or a stronger application design.

3. Does the cache key ignore headers that change the response?

If a request header changes the selected JSON, shared caches need that dimension in their cache key, commonly through Vary or trusted edge configuration. Missing language, encoding, media type, or API-version variation can serve the wrong representation. Keep variation minimal because unnecessary dimensions destroy cache reuse and increase operational complexity.

Suppose one URL returns localized content based on Accept-Language:

Vary: Accept-Language, Accept-Encoding

Without that variation, the first cached language can reach later callers. The MDN Vary reference states that Vary identifies request headers influencing a response and should remain consistent for a URL, including on 304 Not Modified responses.

Version headers also need separation. If X-API-Version selects a different schema but the CDN ignores it, a v1 client may receive v2 JSON. The JSON API versioning mistakes guide explains why silent version fallback already creates risk; shared caching amplifies it.

Do not put every request header into Vary. Values such as User-Agent can fragment storage into many variants, reducing hit rates and making eviction unpredictable. Normalize negotiable values where possible and document which dimensions affect representation.

Query parameters belong in the URL but still require review. A CDN rule that ignores ?fields=, ?include=, or filters can collapse different response bodies into one key.

4. Does one ETag represent multiple JSON representations?

Generate validators for the selected representation, not only the underlying database row. Compression, field selection, localization, authorization filtering, embedded relationships, and API version can produce different bytes or semantics. A validator reused across variants can incorrectly return 304 Not Modified when the client's cached representation does not match its current request.

A typical conditional request is:

If-None-Match: "customer-42-en-v17"

If the selected representation is unchanged, the server returns 304 with no body; otherwise it returns 200 JSON and the current ETag. The MDN conditional request guide explains this validation flow.

Strong and weak validators are different. The ETag reference describes W/ as a weak validator for semantically equivalent representations rather than byte-for-byte identity. Pick the form appropriate to the operation and do not remove quotes while storing values.

ETags can also protect updates with If-Match, returning 412 Precondition Failed when another client changed the resource. That concurrency use is related but distinct from cache validation with If-None-Match.

Before relying on that write protection, run these JSON API concurrency tests for conditional updates; a working conditional GET does not prove that mutation routes enforce the same validator atomically.

GitHub recommends conditional GET requests where supported and notes that an authorized request returning 304 can avoid primary rate-limit cost under its current policy. That is provider-specific behavior, not a general HTTP guarantee.

Conditional requests can reduce transferred data and provider work, but they do not replace client pacing. The JSON API rate-limit mistakes guide covers Retry-After, jitter, concurrency, and bounded retry behavior when a service still throttles requests.

Invalidate or version every affected cache entry when a mutation changes data, including collection pages, summaries, counts, embedded resources, and permission-filtered variants. Purging only the detail URL leaves stale lists and dashboards. Prefer versioned keys or bounded freshness when complete dependency tracking would be unreliable or operationally fragile.

Updating /customers/42 may also affect:

  • /customers?page=1;
  • /customers?status=active;
  • /dashboard/summary;
  • /accounts/7?include=customers;
  • a search result containing the customer's old name.

A purge system must know those relationships or accept bounded staleness. Wildcard purges are simple but can remove useful cache content and create an origin traffic spike. Versioned namespaces avoid complex deletion but require clients or edge logic to reference the current version.

Plan failure behavior. What happens if the database commit succeeds but the purge call fails? An outbox record written in the same transaction can make invalidation retryable. Monitor purge age and failures rather than assuming the CDN call always succeeds.

Do not cache mutation responses simply because they contain JSON. Most practical HTTP caching centers on GET and HEAD; other methods have narrower rules and inconsistent implementation support. Treat write responses and the invalidation they trigger as separate concerns.

6. Can cached errors hide recovery or new resources?

Set explicit caching policy for errors instead of assuming only successful JSON is stored. A cached 404 can hide a newly created resource, and a stored transient 500 can extend an outage after the origin recovers. Choose short deliberate freshness or no-store, preserve the correct variation, and test recovery through each intermediary.

HTTP permits caching for more statuses than many developers expect. The current MDN cacheability reference lists several cacheable status codes, including 404 under applicable conditions.

Negative caching can be useful. A short cache for a nonexistent immutable identifier can protect the origin from repeated probes. It is unsafe when the resource may appear seconds later and the client expects immediate read-after-write behavior.

Authentication errors deserve special care. Never let a shared cache reuse one caller's 401 or 403 for another caller with different credentials. Ensure authorization-related request differences cannot collapse into one entry, or mark the response unstoreable.

Test a realistic sequence: request a missing resource, create it, then request it again through the same CDN location and client headers. Separately inject a temporary origin failure and verify that success becomes visible when expected.

7. Do tests bypass the proxy or CDN that actually caches responses?

Origin unit tests cannot prove production caching behavior. Run end-to-end tests through the deployed proxy or CDN using multiple identities, variants, conditional requests, writes, and errors. Inspect Age, cache-status headers, Cache-Control, Vary, and validators, then verify bodies and authorization context rather than trusting a reported cache hit.

A practical test matrix includes:

ScenarioExpected result
Same public GET twiceSecond may be a safe hit
Two authenticated usersNo cross-user body reuse
Different languagesCorrect body for each variant
Matching If-None-Match304 with no JSON body
Write then readFresh value within documented bound
404 then createResource appears on schedule
Temporary 500 then recoveryError does not outlive policy

Log safe cache decisions with endpoint family, status, hit or miss, age, validator presence, and variant names. Do not log authorization values or sensitive response bodies.

For background, a web cache stores web documents to reduce retrieval work and latency. Wikipedia is not the authority for your edge configuration; current HTTP documentation and observed production headers must drive verification.

Test changes in a staging path configured like production, then release gradually. A cache rule can affect every caller instantly, so rollback should be a configuration operation with known propagation time.

These seven checks form one cache review: protect private data, choose clear directives, vary on representation inputs, generate correct validators, invalidate dependencies, control error storage, and test the real delivery path. Speed is useful only after correctness and isolation are proven.

Frequently Asked Questions

Should JSON API responses be cached?
Cache JSON responses when the data, authorization model, and freshness requirements allow it. Public reference data often benefits from shared caching, while personalized or sensitive responses need private caching or no storage. Define policy per endpoint and representation instead of applying one global header to every API response.
What is the difference between no-cache and no-store?
Cache-Control: no-cache permits a response to be stored but requires validation before reuse. Cache-Control: no-store tells caches not to store the response. Use no-store when storage itself is unacceptable, and no-cache with ETag or Last-Modified when storage is useful but every reuse must be checked.
How does an ETag work with a JSON API?
The server returns an ETag representing a selected resource representation. A client later sends that value in If-None-Match. If the selected representation is unchanged, the server can return 304 Not Modified without a response body; otherwise it returns the current JSON and a new validator.
Should authenticated API responses use public caching?
Not by default. Responses selected using credentials, cookies, tenant identity, or permissions can expose one user's data to another through a shared cache. Mark personalized responses private or no-store unless the response is intentionally identical and the shared-cache policy is explicitly designed, reviewed, and tested for authorization boundaries.
Can a JSON API cache 404 or error responses?
Some error statuses can be cacheable, so define explicit policy. A cached 404 may hide a resource created moments later, while caching a transient 500 can prolong an outage. Use short, deliberate freshness or no-store as appropriate, preserve relevant Vary dimensions, and test error recovery through the actual CDN or proxy.

Inspect the cached JSON representation

Format a non-sensitive response and compare its body with Cache-Control, Vary, ETag, age, and authorization context.

Open JSON Prettifier