Skip to main content

Caching

4 min read

This page covers the cache option: what Cache-Control an object is served with, where to set it, and which value to pick.

Cache-Control is written once, at upload, and stored with the object. The CDN and the browser honor it on every read. There is no per-request override; changing it means writing the object again.


Which value to pick#

ObjectPathcacheHow readers see a change
Unique path per upload (uniquePath)new each time'immutable'the path is new, nothing to invalidate
Stable path that changes rarelystable'immutable' plus versionedUrlthe URL changes with the etag
Fixed URL you do not control, or a client that drops query stringsstable'revalidate'a 304 check on every read
Private or sensitiveany'no-store', or a short durationthe link expires; see below

The cache option#

ValueStored header
'immutable'public, max-age=31536000, immutable
'revalidate'public, max-age=0, must-revalidate
'no-store'no-store
a duration ('15m', 3600)public, max-age=<seconds>
unsetpublic, max-age=3600
anything containing = or ,stored exactly as written

A duration is converted to whole seconds, so '1500ms' stores max-age=1. The grammar is on Types.

The raw header#

Anything containing = or , is treated as a raw header and stored as written. Use this for s-maxage, stale-while-revalidate, no-transform and anything else the three keywords do not cover.


revalidate versus a short max-age#

cache: 'revalidate'cache: '60s'
Unchanged object304, no bodyfull object, once a minute
Object just overwrittennext read sees itup to 60 s of the old bytes

'revalidate' stores public, max-age=0, must-revalidate. The cached copy is checked with If-None-Match on every read, so an unchanged object costs a 304 with no body. A short max-age serves stale bytes until it expires, then re-downloads the whole object.

'revalidate' costs a round trip per read, but is never stale and never downloads the bytes twice.


Where you can set it#

Four places. The most specific one wins.

On the bucket#

lib/blob.ts

The default for every object this bucket stores.

On a put#

updateJson takes it too, for the object it rewrites. So do copy and move, for the destination. Without it the source's value carries over.

On a signed upload URL#

Signed into the URL and handed back in headers, so the uploader has to send it verbatim.

On a direct browser upload#

lib/uploads.ts

Decided per upload on your server and signed into the presigned PUT. See Upload handler.


Private buckets#

cachePublic bucketPrivate bucket
unsetpublic, max-age=3600private, max-age=3600
'1m'public, max-age=60private, max-age=60
'immutable'public, max-age=31536000, immutableprivate, max-age=31536000, immutable
'revalidate'public, max-age=0, must-revalidateprivate, max-age=0, must-revalidate
'no-store'no-storeno-store

On a private bucket, private replaces public, so no shared cache keeps a copy of an object only a signed request may read. This follows the bucket's visibility in the console; nothing in the code declares it.

A raw header string is passed through as written, visibility included: cache: 'public, max-age=60' on a private bucket stores public, max-age=60.


Immutable plus a versioned URL#

app/api/avatar/route.ts

versionedUrl is url with the etag on the query, so it changes whenever the content does. A stable path stored immutable and served through versionedUrl is cached for a year, and every overwrite produces a URL no cache has seen. The path never moves, so nothing has to be deleted.

url and versionedUrl are both undefined on a private bucket.


no-store and signed reads#

These are two separate mechanisms. The link expires at expiresAt, but the stored Cache-Control outlives it: with a long max-age the reader's browser keeps the bytes after the link stops working. If a reader must not keep the bytes, store the object with no-store.

no-store drops the visibility scope entirely and stores no-store on public and private buckets alike.

See signedReadUrl for link lifetimes.


What the upload route itself caches#

An upload route's GET serves its constraints document with a 60 second Cache-Control of its own, unrelated to the objects the route stores. See Constraints.