skip to content

How do you serve pre-compressed static assets over HTTP — for example .br and .gz files written at build time — and what must be correct for caches and proxies?

level: seniorimportance: should knowfreq 28%

answer

  1. build once at max level, serve as a file
  2. Content-Type from the original, not .br
  3. Vary: Accept-Encoding, always
  4. distinct strong ETag per encoding
  5. keep the identity file as fallback

basics

~20 s

Build both a .br and .gz copy beside each asset. The server checks Accept-Encoding, serves the matching file with the original Content-Type, the right Content-Encoding, Vary: Accept-Encoding, a distinct ETag per encoding, and falls back to the uncompressed file.

solid answer

~50 s

At build time you emit `app.js`, `app.js.br` and `app.js.gz`. At request time the server inspects `Accept-Encoding`, picks the best variant whose file exists, and serves it as if it were the original resource: ``` Content-Type: text/javascript Content-Encoding: br Vary: Accept-Encoding ETag: "4f2c-br" ``` The rules that make this safe: - **`Content-Type` comes from the original asset**, not from the `.br` extension. Serving `application/x-brotli` breaks the client. - **`Vary: Accept-Encoding`** so shared caches store each variant separately. - **A distinct ETag per encoding** — the variants are different octet sequences, so sharing a validator corrupts conditional requests through a cache. - **Always keep the identity file** for clients that send no `Accept-Encoding`. - **Content-Length is the compressed file's size**, which the server gets right automatically by stat-ing the variant. The payoff: Brotli level 11 becomes affordable because you pay for it once per deploy, not once per request — and the serving path is a plain static file read with no CPU cost.

code

http · 11 lines
http
GET /static/app.4f2c.js HTTP/1.1
Host: cdn.example.com
Accept-Encoding: gzip, br

HTTP/1.1 200 OK
Content-Type: text/javascript
Content-Encoding: br
Content-Length: 265011
ETag: "4f2c-br"
Vary: Accept-Encoding
Cache-Control: public, max-age=31536000, immutable

go deeper

for a junior

Know that the compressed copies are built ahead of time and the server picks one based on Accept-Encoding, keeping the original Content-Type.

for a middle

Add Vary: Accept-Encoding, the identity fallback, and that Content-Length is the compressed file's size.

for a senior

Cover per-encoding ETags and the conditional-request corruption they prevent, stale-variant build hygiene, CDN double-compression, and range-request behaviour.

for a principal

Weigh build-time cost and artifact-count growth against per-request CPU and egress, decide which layer owns compression across the estate, and set the policy for adopting new codings without invalidating caches.

## The idea Compressing a static asset on every request is wasteful: the bytes never change. Instead the build step emits the compressed variants next to the original, and the web server picks a file rather than running a compressor. ``` dist/app.4f2c.js 1 200 000 bytes dist/app.4f2c.js.gz 310 000 bytes dist/app.4f2c.js.br 265 000 bytes dist/app.4f2c.js.zst 272 000 bytes ``` Because encoding happens once per deploy, you can use the slowest, best settings — Brotli quality 11, zstd 19 — that would be unthinkable inside a request. And the request path becomes a plain file read: no compressor, no per-request CPU, and `sendfile`-style zero-copy serving stays available. ## Server configuration Every mainstream server supports this: nginx `gzip_static on;` and the Brotli module's `brotli_static on;`, Caddy's `precompressed`, Apache via `mod_deflate` rewrite rules, and every CDN as a toggle. The logic is: parse `Accept-Encoding`, walk your preference order (`br`, then `zstd`, then `gzip`), serve the first variant whose file exists, else serve the identity file. ## The five things that must be right **1. Content-Type is the original type.** The response is still JavaScript; the `.br` suffix is a storage detail. If you let the static handler infer the type from the file extension you will emit something like `application/octet-stream` and the browser will refuse to execute it. This is the single most common bug in hand-rolled implementations. **2. Content-Encoding names the coding actually used.** `br`, `gzip`, `zstd` — matching the file you chose. **3. `Vary: Accept-Encoding` on every such response.** The body genuinely depends on that request header. Without it a shared cache or CDN will serve the Brotli bytes to a gzip-only client, or hand a compressed body to a client that sent no `Accept-Encoding`. The failure is intermittent and user-subset-specific, which makes it painful to diagnose. **4. A distinct, strong ETag per variant.** RFC 9110 ties a strong validator to the exact octet sequence. If `app.js.gz` and `app.js.br` share `"4f2c"`, then a client holding the gzip variant can send `If-None-Match: "4f2c"`, get `304 Not Modified` from a cache that is actually holding the Brotli variant, and end up decoding the wrong bytes. Conventionally the encoding is suffixed into the tag. A related trap: some servers derive the ETag from the *file* mtime/size, which naturally differs per variant — that works, but only if the derivation runs on the variant actually served. **5. Keep the identity file.** Clients with no `Accept-Encoding`, oddball middleboxes, and internal tooling need it. Deleting the uncompressed original to save space is a false economy. ## Interactions to watch - **Range requests.** A `Range` header addresses the *encoded* bytes. Precompressed variants are fixed files, so ranges are consistent and safe — which is actually an advantage over on-the-fly compression, where the byte offsets can shift between responses. - **Cache-Control.** Precompressed assets are normally content-hashed in the filename, so they get `Cache-Control: public, max-age=31536000, immutable`. The hash covers the *source* content; all three encodings of one hash are the same resource in different codings. - **Build hygiene.** A stale `.br` next to a fresh `.js` serves outdated code to most users and correct code to the rest — a genuinely confusing incident. Generate the variants in the same build step that produces the asset, never as a separate optional job, and fail the build if a variant is missing. - **CDN double-compression.** If the CDN also compresses, disable one side. Otherwise you can end up with a re-encoded body, or the CDN storing an identity copy it re-compresses at lower quality, silently discarding your level-11 work. - **Small files.** Below about 1 KB the compressed variant may be larger; a build step should skip emitting a variant that is not smaller, and the server naturally falls back. ## Verifying it `curl -sI -H 'Accept-Encoding: br' https://…/app.js` should show `Content-Encoding: br`, the JavaScript `Content-Type`, `Vary: Accept-Encoding`, and a size matching the `.br` file. Repeat with `gzip` and with the header omitted; you should see three different sizes and three different ETags, with the same URL throughout.

  • Why must the gzip and Brotli variants of the same asset carry different ETags?
    A strong ETag identifies an exact octet sequence, and the two variants are different byte sequences. Sharing a tag lets a conditional request validate against the wrong variant — a client holding gzip bytes gets a 304 from a cache holding Brotli, and then decodes garbage. Suffix the encoding into the tag.
  • What goes wrong if the static handler derives Content-Type from the .br filename?
    It emits something like application/octet-stream or a Brotli-specific type instead of text/javascript, so the browser refuses to execute the script or the client refuses to parse the JSON. The Content-Type must always describe the payload after decoding, taken from the original asset.
  • Your CDN also compresses. Should you keep precompressing at build time?
    Yes, but disable compression at one of the two layers. Precompressed Brotli-11 is smaller than anything a CDN will produce on the fly, so prefer serving your artifacts through and turning the CDN's own compression off — otherwise you risk double encoding or the CDN re-compressing at a lower quality and discarding your build-time work.

Like a bakery pre-slicing loaves overnight: the expensive work happens once, off the critical path, and the counter staff just hand over whichever pre-prepared form the customer can carry.

saying these in an interview costs you the question

  • Deriving Content-Type from the .br or .gz file extension.
  • Serving precompressed variants without Vary: Accept-Encoding.
  • Using one ETag for the identity, gzip and Brotli variants.
  • Deleting the uncompressed original to save disk space.
  • Generating variants in a separate optional job, allowing stale .br files beside fresh assets.

context