Caching & Conditional Requests
Cache-Control, ETags and conditional requests: cheap revalidation with 304, safe concurrent updates with If-Match and 412, and Vary on Api-Version.
HTTP already has a complete caching and concurrency model in RFC 9110 and RFC 9111. Use it instead of inventing one: every client, CDN and proxy already understands it.
Cache-Control
Every response says how it may be cached. No header means a cache gets to guess.
| Response | Cache-Control |
|---|---|
| Account-specific data (most API responses) | private, no-cache |
| Public, slowly changing data | public, max-age=300 |
| Immutable data (a versioned asset, a settled ledger entry) | public, max-age=31536000, immutable |
| Anything with secrets or card data | no-store |
no-cache doesn't mean "don't cache". It means "revalidate before reuse", which pairs with ETags below. no-store is the one that forbids storing.
Validators: ETag and Last-Modified
Every GET on a resource returns an ETag, and a Last-Modified where the resource has a meaningful update time.
HTTP/1.1 200 OK
ETag: "a3f1c9"
Last-Modified: Thu, 22 Oct 2026 14:23:45 GMT
Cache-Control: private, no-cache
- Strong ETags (
"a3f1c9") when the bytes are identical. A hash of the response body, or the row's version number, both work. - Weak ETags (
W/"a3f1c9") when two representations are equivalent but not byte-identical (different compression, key order).
The syntaqx API does this on every deterministic GET. /healthz and /openapi return an ETag hashed from the body, a timestamp for a given t is immutable, and generated values like /uuid are no-store.
Revalidation: 304 Not Modified
Clients send the validator back. If nothing changed, the response is an empty 304:
GET /payments/01932c6c-7616-7590-a8db-cb4b0e4d1c8a
If-None-Match: "a3f1c9"
HTTP/1.1 304 Not Modified
ETag: "a3f1c9"
Make revalidation cheap for clients to adopt. GitHub doesn't count an authorized request that returns 304 against the primary rate limit, and that's the policy here too: polling with If-None-Match should be the path of least resistance.
Optimistic Concurrency: If-Match
Two clients read the same resource, both edit it, the second write silently overwrites the first. Conditional writes prevent the lost update:
PATCH /customers/0194a7f3-2b11-7e80-9c3d-4e8f1a2b5c70
If-Match: "a3f1c9"
Content-Type: application/merge-patch+json
{ "email": "alice@example.com" }
| Situation | Response |
|---|---|
| ETag matches | 200, with the new ETag |
| ETag doesn't match, someone else changed it | 412 Precondition Failed |
If-Match required but missing |
428 Precondition Required (RFC 6585) |
On 412, the client re-reads, reapplies its change, and tries again. If-Match is required on resources where a lost update is costly (configuration, settings, anything edited by humans in two tabs). In the database, the check is the same guarded update as in Payment States & Ledgers: UPDATE ... WHERE id = $1 AND version = $2.
Vary
A cache keys on the URL by default. Any request header that changes the response must be listed in Vary, or a cache will serve one client's representation to another:
Vary: Accept, Api-Version, Accept-Encoding
Acceptbecause of content negotiation.Api-Versionbecause each version can shape the response differently (Versioning).Authorizationresponses should beprivateinstead of relying onVary: Authorization, which shared caches handle poorly.
Don't Cache Errors by Accident
4xx and 5xx responses get Cache-Control: no-store unless the error is genuinely stable (a 404 on an ID that can never exist, a 410 after sunset). A cached 503 turns a ten-second blip into a ten-minute outage.
References
Specifications
- RFC 9110: HTTP Semantics, validators, conditional requests and
412 - RFC 9111: HTTP Caching,
Cache-Controland cache keys - RFC 6585: Additional HTTP Status Codes,
428 Precondition Required
Reference
- MDN: HTTP caching, how browsers and shared caches behave
- MDN: HTTP conditional requests, revalidation and lost-update prevention
- MDN: Cache-Control, every directive in one place
- MDN: ETag, strong and weak validators
In practice
- GitHub Docs: Best practices for using the REST API, conditional requests that don't count against the rate limit
- Google AIP-154: Resource freshness validation, ETags on resources
- Microsoft: Azure REST API Guidelines, conditional requests and optimistic concurrency