← Notes

Cache-Control will not save you from a bandwidth bill

This one started as a billing alert.

A Next.js site on Vercel had ten short testimonial videos in the public folder. For a long time that was fine — about 32 GB of bandwidth a month, nothing anyone looked at. Then, over nineteen days, it moved 32 TB. At Vercel’s overage rate that came to a four-figure bill for a page that had not changed.

The team had already done the reasonable things by the time it reached me. preload="auto" was gone from the <video> tags. Cache-Control headers were set. Neither made any difference, and the dashboard graph kept climbing.

Why the headers were never going to work

Chrome DevTools showed the videos coming from disk cache on the second load, which felt like proof the fix was working. It was proof of something narrower: one returning user, on one browser, didn’t re-download. Every first visit, every other browser, and every bot and scraper fetched the full file. Cache-Control controls what a client does on the next request. Vercel bills every byte that leaves its edge, including all the first ones.

The traffic pattern told the rest. Each video was getting ~27,000 requests in twelve hours and moving 80–92 GB. That’s not an audience; that’s hotlinking, scraping, or both.

A few other “fixes” would have been worse:

  • A Next.js rewrite to a CDN proxies the whole response through Vercel. You pay the CDN and Vercel.
  • ISR and static generation apply to HTML, not binary assets.
  • Compression does nothing to an MP4 that’s already compressed.

The only thing that reduces the bill is bytes not passing through Vercel’s network at all.

This is the part worth sitting with, because it’s counterintuitive if you come from a performance mindset. Everything the frontend toolkit offers — caching, preloading, prefetching, compression — is about what a user experiences. None of it is about what the edge charges. They look like the same lever and they aren’t.

What we shipped, in order

Same afternoon. A middleware 301 redirect for the video paths to a CloudFront distribution, with the files in S3 behind Origin Access Control. A redirect response is a couple hundred bytes; the browser fetches the video from CloudFront directly. It has to be NextResponse.redirect, not rewrite — a rewrite recreates the problem with an extra hop.

Same afternoon, in parallel. A WAF rate limit on the video paths (fifty requests per IP per minute is generous for humans and hostile to scrapers) and a spend budget with an alert threshold.

Next sprint. A cdnUrl() helper, components pointed at CloudFront directly, middleware deleted. The redirect was a bridge, not an architecture.

We stayed on AWS deliberately. Cloudflare R2’s zero-egress pricing is real and attractive, but this platform operates under a BAA with AWS, and Cloudflare doesn’t offer a self-service one. Even for public assets with nothing sensitive in them, one cloud with one agreement is easier to reason about than two.

The numbers

Vercel Pro overage runs about $150 per TB. CloudFront pay-as-you-go is around $85 at these volumes, and CloudFront’s flat-rate plan makes the traffic nearly free. At the observed rate the projected saving was between 97% and 99.7%. The exact percentage matters less than the shape: this isn’t an optimization, it’s moving a workload to the tool built for it.

What I’d tell my past self

Vercel has a conformance rule called NEXTJS_NO_SELF_HOSTED_VIDEOS. The platform was already telling us not to do this; we just weren’t listening in the place where it says it.

More generally: know what you’re billed for, not what you think you’re optimizing. Caching, preloading and compression are performance tools. They change what a user experiences. They do not change what an edge network charges you for, and confusing the two is how a page that hasn’t changed in a year sends you an invoice nobody budgeted for.

And set the spend alert before the month you need it.