How does HTTP caching work with Cache-Control, ETag and conditional requests, and how does URLCache fit in?
Predict first
A client resends the ETag it received as If-None-Match. What does a server stub that supports it answer, and what did the second request send?
Stub.handler = { req in
if req.value(forHTTPHeaderField: "If-None-Match") == "\"v1\"" { return (304, [:], Data()) }
return (200, ["ETag": "\"v1\""], Data("body".utf8))
}
var req = URLRequest(url: URL(string: "https://example.test/etag")!)
let (_, r1) = try await session().data(for: req)
req.setValue("\"v1\"", forHTTPHeaderField: "If-None-Match")
let (_, r2) = try await session().data(for: req)
print((r1 as! HTTPURLResponse).statusCode, (r2 as! HTTPURLResponse).statusCode)
- 200 200
- 200 304
- 304 304
Predict first
A response is stored in a URLCache and read back:
import Foundation
let cache = URLCache(memoryCapacity: 1_000_000, diskCapacity: 0)
let url = URL(string: "https://example.test/c")!
let resp = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: ["Cache-Control": "max-age=60"])!
cache.storeCachedResponse(CachedURLResponse(response: resp, data: Data("cached".utf8)), for: URLRequest(url: url))
print(String(data: cache.cachedResponse(for: URLRequest(url: url))!.data, encoding: .utf8)!, cache.currentMemoryUsage > 0)
- cached true
- cached false
- It returns nil
Mental model. An ETag is a version stamp on a document. Instead of re-reading it, you ask “is it still version 7?”, and the answer “yes” costs one line instead of the whole document.
HTTP caching is driven by response headers. Cache-Control tells the client how long a response is fresh (max-age=60) and whether it may be stored at all (no-store means never; no-cache means “store it, but revalidate before reuse”; private and public say who may store it). While a stored response is fresh the client reuses it with no network request. When it is stale, the client revalidates with a conditional request: the server’s ETag (an opaque version tag) comes back as If-None-Match, or Last-Modified as If-Modified-Since. If nothing changed the server answers 304 Not Modified with no body, which is cheap, and the client reuses its stored copy; otherwise it sends a new 200 with the body and a new ETag.
URLSession implements this for you through URLCache and the request’s cachePolicy (default .useProtocolCachePolicy, which follows the headers): a default session has a shared on-disk cache (URLCache.shared, small by default: roughly half a megabyte in memory and a few tens of megabytes on disk, so raise it for image-heavy apps), and an ephemeral session keeps it in memory. Know the policies: .reloadIgnoringLocalCacheData bypasses it, .returnCacheDataElseLoad uses any cached copy, .returnCacheDataDontLoad is offline-only. Practical rules. Make the server send correct cache headers and ETags (client tricks are second best). When you build conditional requests yourself (your own cache, a database), store the ETag with the data and send If-None-Match; handle 304 as “keep what I have” and note that URLSession hides a 304 from you only when its own cache handled the revalidation. Never cache authenticated or personal responses in a shared cache (Cache-Control: private), and key a cache by the full URL including query string. Image URLs deserve long max-age with versioned names.
The first request has no validator and gets the full response. The second proves it already has version v1, so the server answers 304 with no body.
The cache keys the stored response by request URL and returns it, and the memory usage reflects the stored data.
Common traps
- Treating a 304 as an error or an empty body. It means “use your stored copy”.
- Relying on the tiny default
URLCachefor a media-heavy app. Entries are evicted constantly; size it deliberately.
They might follow up with
- What does
no-cachemean? You may store it, but you must revalidate before reuse;no-storeforbids storing. - How do you force a fresh fetch?
cachePolicy = .reloadIgnoringLocalCacheDataon the request.
Related questions
- 12.3.1 How do you decode and paginate an API response, and which pagination style is safest?
- 12.7.1 How do you design an image loading pipeline: decoding, downsampling, caching and de-duplication?
- 2.7.2 How would you design an in-memory image cache?
← How do you decode and paginate an API response, and which pagination… How do you handle access-token expiry, refresh races and 401 loops? →