Quick answer: Safe JSON API caching requires an endpoint-specific storage policy, correct
Cache-Controlsemantics, 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.
5. Do writes leave related cached JSON stale?
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:
| Scenario | Expected result |
|---|---|
| Same public GET twice | Second may be a safe hit |
| Two authenticated users | No cross-user body reuse |
| Different languages | Correct body for each variant |
| Matching If-None-Match | 304 with no JSON body |
| Write then read | Fresh value within documented bound |
| 404 then create | Resource appears on schedule |
| Temporary 500 then recovery | Error 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.