ETag Revalidation CacheLoading saved progress…

ETag Revalidation Cache

An ETag revalidation cache stores each response alongside its ETag — a short version stamp the server assigns to the current content — and on the next read revalidates instead of blindly re-downloading: it asks the server whether the copy it already holds is still current, quoting that stamp. When nothing has changed the server replies 304 Not Modified with no body and you reuse the cached copy; when it has changed the server sends a fresh 200 with new data and a new ETag. It is how browsers and HTTP caches avoid re-fetching a resource that has not moved.

Implement etagRevalidationCache(fetcher). It returns get(url), which resolves the resource's data. Under the hood get calls fetcher(url, { etag }), passing the stored ETag (or undefined on the very first read), and turns the 200 or 304 answer into the right cached value.

Signature

type Response<T> =
  | { status: 304 }                          // Not Modified — no body
  | { status: 200; data: T; etag: string };  // fresh body + its version stamp

type Fetcher<T> = (
  url: string,
  opts: { etag: string | undefined },        // the stored ETag, if any
) => Promise<Response<T>>;

function etagRevalidationCache<T>(
  fetcher: Fetcher<T>,
): (url: string) => Promise<T>;               // get(url) -> resolves the data

Examples

const get = etagRevalidationCache(fetcher);

await get('/doc'); // cold: fetcher('/doc', { etag: undefined })
                   //   -> { status: 200, data, etag: 'v1' }
                   // stores { data, etag: 'v1' }, resolves data

await get('/doc'); // revalidate: fetcher('/doc', { etag: 'v1' })
                   //   -> { status: 304 }
                   // resolves the SAME cached data, nothing re-downloaded
// the document has changed on the server since 'v1'
await get('/doc'); // fetcher('/doc', { etag: 'v1' })
                   //   -> { status: 200, data: next, etag: 'v2' }
                   // replaces the cache, resolves the new data;
                   // the next read now sends 'v2'

Notes

  • Send the stored ETag — after the first read, every read passes the cached ETag as { etag }; only the very first read for a url sends { etag: undefined }.
  • 304 means reuse — a { status: 304 } response has no body; resolve the data already in the cache and leave the stored ETag untouched.
  • 200 means replace — a { status: 200 } response carries fresh data and a new etag; overwrite both in the cache and resolve the new data.
  • Dedupe in-flight reads — concurrent get(url) calls for the same url must share one fetch; a second call that arrives while one is running gets the same promise.
  • Every read revalidates — unlike a plain cache you never serve a cached value without checking; the ETag is what makes that check cheap.
  • Failures do not poison — if the fetch rejects, reject the caller and cache nothing, so the next read retries.

FAQ

What is an ETag and where does it come from?
An ETag is a short opaque version stamp the server assigns to a resource's current content, often a hash of the body. The server returns it on a 200 response and compares it against the client's next request to decide whether anything changed.
How is this different from a time-to-live cache?
A TTL cache trusts a value until a timer expires and can serve stale data in the meantime. An ETag cache revalidates on every read, but the server answers a 304 cheaply when nothing changed, so it is both fresh and low-bandwidth.
What does a 304 Not Modified response actually save?
The response body. The network round-trip still happens, but the server sends only headers, so the client reuses the data it already cached instead of re-downloading an identical payload.
Loading editor…