Conditional Requests
CONDITIONAL REQUESTS LET CLIENTS MAKE HTTP REQUESTS CONTINGENT ON RESOURCE STATE — PREVENTING REDUNDANT TRANSFERS WITH 304 NOT MODIFIED AND ENABLING OPTIMISTIC CONCURRENCY CONTROL WITH 412 PRECONDITION FAILED. WITHOUT CONDITIONAL REQUESTS, EVERY GET TRANSFERS THE FULL BODY AND EVERY PUT RISKS OVERWRITING CONCURRENT CHANGES.
When to Use
- Implementing cache revalidation for a polling client that frequently re-fetches the same resource
- Adding optimistic concurrency control to a PUT or PATCH endpoint to prevent lost updates
- Preventing duplicate creation with a conditional PUT (
If-None-Match: *) - Diagnosing 304 or 412 responses from an HTTP client or CDN
- Implementing efficient polling where clients receive a 304 when nothing has changed
- Deciding between ETag-based and timestamp-based conditional logic
- Building a file storage API (S3-style) where overwrite protection is required
- Reviewing code that re-fetches an entire resource list on every interval without conditional headers
Instructions
Key Concepts
If-None-Match — The client sends a stored ETag value; the server returns
304 Not Modifiedif the current ETag matches (the resource has not changed), or200 OKwith a new body and ETag if it has changed. Used on GET/HEAD requests for cache revalidation. Also supportsIf-None-Match: *to check for resource non-existence (useful for conditional creation with PUT).GET /issues/42 If-None-Match: "etag-v7"If unchanged:
HTTP/1.1 304 Not Modified ETag: "etag-v7" Cache-Control: max-age=60If changed:
HTTP/1.1 200 OK ETag: "etag-v8" Content-Type: application/json { "id": 42, "title": "Updated title", ... }If-Modified-Since — The client sends a stored
Last-Modifiedtimestamp; the server returns304 Not Modifiedif the resource has not changed since that time. Weaker than ETags (second-level granularity). When bothIf-None-MatchandIf-Modified-Sinceare present,If-None-Matchtakes precedence (RFC 9110).GET /releases/latest If-Modified-Since: Thu, 09 Apr 2026 14:30:00 GMTIf-Match — The client sends a stored ETag; the server proceeds only if the current ETag matches. If the ETag has changed (someone else modified the resource), the server returns
412 Precondition Failed. Used on PUT/PATCH/DELETE to implement optimistic concurrency control — the write is rejected if the resource was modified since the client last read it.PATCH /orders/ord_123 If-Match: "etag-v3" Content-Type: application/merge-patch+json { "status": "shipped" }If ETag still matches (no concurrent modification):
HTTP/1.1 200 OK ETag: "etag-v4"If ETag changed (concurrent write detected):
HTTP/1.1 412 Precondition Failed Content-Type: application/problem+json { "type": "https://api.example.com/errors/conflict", "title": "Precondition Failed", "detail": "The resource was modified by another client. Re-fetch and retry." }If-Unmodified-Since — Timestamp-based equivalent of
If-Match. Less precise than ETag-basedIf-Match. The server proceeds only if the resource has not been modified since the specified time.If-None-Match: * — A special form that tests for non-existence. The server proceeds only if the resource does not exist. Used with PUT to implement safe creation: create the resource only if no resource exists at this URL. Returns
412 Precondition Failedif a resource already exists.PUT /configs/feature-flags If-None-Match: * Content-Type: application/json { "darkMode": false, "betaFeatures": [] }304 Not Modified — The server's response when a conditional GET/HEAD condition evaluates to "not changed." The response has no body; the client uses its cached copy. The response must still include relevant headers:
ETag,Last-Modified,Cache-Control,Vary,Expires. OmittingLast-Modifiedbreaks clients usingIf-Modified-Sinceon subsequent requests. This is the primary bandwidth-saving mechanism for polling APIs and CDN revalidation.
Worked Example
GitHub's REST API implements both 304-based cache revalidation and ETag-based write protection:
Initial fetch — store ETag and Last-Modified for future requests:
GET /repos/github/docs/contents/README.md
Authorization: Bearer ghp_...
HTTP/1.1 200 OK
ETag: "abc123"
Last-Modified: Wed, 08 Apr 2026 10:00:00 GMT
Cache-Control: private, max-age=60
Content-Type: application/vnd.github.v3+json
{ "name": "README.md", "sha": "abc123", "content": "...", "encoding": "base64" }
Polling revalidation — conditional GET with stored ETag:
GET /repos/github/docs/contents/README.md
Authorization: Bearer ghp_...
If-None-Match: "abc123"
HTTP/1.1 304 Not Modified
ETag: "abc123"
Cache-Control: private, max-age=60
No body — the client reuses the cached response. Bandwidth cost: ~200 bytes (headers only) vs. ~3 KB (full response).
Update file with optimistic concurrency (If-Match on PUT):
PUT /repos/github/docs/contents/README.md
Authorization: Bearer ghp_...
If-Match: "abc123"
Content-Type: application/json
{
"message": "Update README",
"content": "<base64-encoded-new-content>",
"sha": "abc123"
}
GitHub's file update API requires the sha field (acting as the ETag equivalent in the request body) to match the current file SHA. If another commit changed the file, GitHub returns 409 Conflict (GitHub's variant of 412 Precondition Failed for this specific endpoint):
HTTP/1.1 409 Conflict
Content-Type: application/json
{ "message": "Update is not a fast forward" }
Conditional creation — prevent overwrite of existing config:
PUT /orgs/myorg/actions/secrets/API_KEY
Authorization: Bearer ghp_...
If-None-Match: *
Content-Type: application/json
{ "encrypted_value": "...", "key_id": "..." }
Proceeds only if the secret does not already exist. Returns 412 or 409 if the secret is already present.
Anti-Patterns
Ignoring ETags on write endpoints. A PUT endpoint that does not require
If-Matchallows the "lost update" problem: two clients read the same resource, both modify it, and the second write silently overwrites the first. For any resource that might be concurrently modified, requireIf-Matchon mutating requests. Return428 Precondition Requiredif the client omitsIf-Matchon a protected resource.Using timestamps instead of ETags for write protection.
If-Unmodified-Sincehas 1-second granularity. Two writes in the same second will not be detected as conflicting. Generate ETags from content hashes for precise conflict detection. Use timestamps only as a fallback when ETag generation is not feasible.Not returning the new ETag after a successful conditional write. After a
PATCHorPUTthat passesIf-Match, the server creates a new resource version. Not returning the updated ETag in the response forces the client to re-fetch the resource to get the new ETag for subsequent writes. Always return the updatedETagheader in the response to mutation requests.Returning 304 without required headers. RFC 9110 requires that a
304 Not Modifiedresponse include the same header fields that would have been sent in a200 OKresponse:Cache-Control,ETag,Vary,Expires. A304missing these headers causes caches to use stale directives from the original response, which may have already expired.
Details
Optimistic vs. Pessimistic Concurrency
| Strategy | Mechanism | Tradeoff |
|---|---|---|
| Optimistic (HTTP) | ETag + If-Match; retry on 412 | No locks; low contention; requires client-side retry logic |
| Pessimistic (locks) | Database row-level lock; held until commit | Prevents conflicts; blocks other writers; lock timeout risk |
For REST APIs, optimistic concurrency via ETags is the standard approach. It scales horizontally without distributed lock coordination. The client reads the resource (gets an ETag), attempts a conditional write (with If-Match), and retries with the new ETag on 412 Precondition Failed. See db-optimistic-locking for database-level implementation.
Conditional Requests and CDN Revalidation
CDNs use conditional requests internally when revalidating stale cache entries with the origin:
Client → CDN (stale cache) → Origin: GET /resource, If-None-Match: "old-etag"
Origin → CDN: 304 Not Modified
CDN → Client: 200 OK (from updated CDN cache)
This means ETag generation on the origin also benefits CDN efficiency — not just direct client interactions. Weak ETags (W/"...") should be used when the response body may be transformed by the CDN (e.g., compressed differently) and the content is semantically equivalent across encodings.
Real-World Case Study: Atlassian Confluence REST API
Atlassian's Confluence REST API adopted If-Match for page update endpoints after a data loss incident: two users editing the same wiki page simultaneously would overwrite each other's changes with no warning. After adding ETag-based optimistic locking, the API rejects stale writes with 409 Conflict and returns the current ETag, prompting the client to re-fetch and present a merge UI. In the first month after deployment, lost-update incidents dropped from approximately 200/week to zero. Polling clients using If-None-Match on frequently accessed pages reduced bandwidth consumption by 65%.
Source
- MDN — HTTP Conditional Requests
- RFC 9110 — HTTP Semantics, Section 13 (Conditional Requests)
- RFC 9110 — HTTP Semantics, Section 8.8 (Validator Fields)
- MDN — ETag
- MDN — If-Match
Process
- Identify the use case: cache revalidation (use
If-None-Match/If-Modified-Sinceon GET) or write conflict detection (useIf-Matchon PUT/PATCH/DELETE). - Generate ETags on the server: hash the response body (MD5 or SHA-256) for strong ETags. Expose
ETagandLast-Modifiedin all GET responses for resources that may be conditionally requested. - For GET endpoints: check
If-None-Matchagainst current ETag; return304 Not Modifiedwith cache headers but no body if matching. - For PUT/PATCH/DELETE endpoints: check
If-Matchagainst current ETag; return412 Precondition Failedwith a problem details body if not matching. Return the updated ETag in the success response. - Run
harness validateto confirm skill files are well-formed.
Harness Integration
- Type: knowledge -- this skill is a reference document, not a procedural workflow.
- No tools or state -- consumed as context by other skills and agents.
- related_skills: api-http-caching, api-http-methods, db-optimistic-locking, api-status-codes
Success Criteria
- GET endpoints return
ETagandLast-Modifiedheaders on all cacheable resource responses. - Conditional GET with a matching
If-None-Matchreturns304 Not Modifiedwith no body and all required cache headers. - PUT/PATCH/DELETE endpoints on concurrently-modifiable resources require
If-Matchand return412 Precondition Failedon ETag mismatch. - Successful mutation responses include the updated
ETagheader for the new resource version. 304 Not Modifiedresponses includeCache-Control,ETag, andVaryheaders per RFC 9110 requirements.