> ## Documentation index
> Fetch the complete documentation index at: https://upstash.com/docs/llms.txt
> Ask it with GET https://upstash.com/docs/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.
> Feature requests: https://upstash.com/docs/common/help/feature-requests.md lists what users asked for and how to vote for a request or add one.
> Page rating: `GET https://upstash.com/docs/feedback?path=/docs/blob/bucket/caching&helpful=true` (or `false`) records whether this page helped. An optional `comment` (URL-encoded, at most 500 characters) says what was missing or wrong. It is anonymous and needs no key. Comments hold no code, secrets or personal data.

# Caching

> immutable, revalidate and no-store, where each one can be set, and what a CDN keeps.

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

<CodeGroup>
```ts TypeScript
await bucket.put("avatars/7.png", file, { contentType: "image/png", cache: "immutable" })
```

```python Python
bucket.put("avatars/7.png", file, content_type="image/png", cache="immutable")
```

```go Go
bucket.Put(ctx, "avatars/7.png", file, &blob.PutOptions{
	ContentType: "image/png",
	Cache:       blob.CacheImmutable,
})
```
</CodeGroup>

`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

| Object | Path | `cache` | How readers see a change |
| --- | --- | --- | --- |
| Unique path per upload ([unique paths](/blob/bucket/writing#unique-paths)) | new each time | `'immutable'` | the path is new, nothing to invalidate |
| Stable path that changes rarely | stable | `'immutable'` plus the versioned URL | the URL changes with the etag |
| Fixed URL you do not control, or a client that drops query strings | stable | `'revalidate'` | a 304 check on every read |
| Private or sensitive | any | `'no-store'`, or a short duration | the link expires; see [below](#no-store-and-signed-reads) |

---

## The `cache` option

| Value | Stored 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>` |
| unset | `public, max-age=3600` |
| anything containing `=` or `,` | stored exactly as written |

<CodeGroup>
```ts TypeScript
cache: "1h"      // public, max-age=3600
cache: 3600      // public, max-age=3600
cache: "15 min"  // public, max-age=900
cache: "7d"      // public, max-age=604800
```

```python Python
cache="1h"                  # public, max-age=3600
cache=3600                  # public, max-age=3600
cache="15 min"              # public, max-age=900
cache="7d"                  # public, max-age=604800
cache=timedelta(minutes=5)  # public, max-age=300
```

```go Go
Cache: "1h"     // public, max-age=3600
Cache: "3600"   // public, max-age=3600
Cache: "15 min" // public, max-age=900
Cache: "7d"     // public, max-age=604800
Cache: "1h30m"  // public, max-age=5400
```
</CodeGroup>

A duration is converted to whole seconds, so `'1500ms'` stores `max-age=1`. The grammar is on [Types](/blob/reference/types#duration).

### The raw header

<CodeGroup>
```ts TypeScript
cache: "public, max-age=60, s-maxage=31536000"
cache: "max-age=0, stale-while-revalidate=86400"
```

```python Python
cache="public, max-age=60, s-maxage=31536000"
cache="max-age=0, stale-while-revalidate=86400"
```

```go Go
Cache: "public, max-age=60, s-maxage=31536000"
Cache: "max-age=0, stale-while-revalidate=86400"
```
</CodeGroup>

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 object | 304, no body | full object, once a minute |
| Object just overwritten | next read sees it | up 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

<CodeGroup>
```ts TypeScript
// lib/blob.ts
import { Bucket } from "@upstash/blob"

export const bucket = Bucket.fromEnv({ cache: "immutable" })
```

```python Python
# app/blob.py
from upstash_blob import Bucket

bucket = Bucket.from_env(cache="immutable")
```

```go Go
// blob.go
bucket, err := blob.NewBucketWith(blob.Options{
	Token: os.Getenv("UPSTASH_BLOB_TOKEN"),
	Cache: blob.CacheImmutable,
})
```
</CodeGroup>

The default for every object this client stores. Client options: [TypeScript](/blob/sdks/ts/overview#the-bucket-client), [Python](/blob/sdks/py/overview#the-bucket-client), [Go](/blob/sdks/go/overview#the-bucket-client).

### On a put

<CodeGroup>
```ts TypeScript
await bucket.put("avatars/7.png", file, {
  contentType: "image/png",
  cache: "revalidate",
})
```

```python Python
bucket.put("avatars/7.png", file, content_type="image/png", cache="revalidate")
```

```go Go
bucket.Put(ctx, "avatars/7.png", file, &blob.PutOptions{
	ContentType: "image/png",
	Cache:       blob.CacheRevalidate,
})
```
</CodeGroup>

The [JSON update](/blob/bucket/writing#updating-json) 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

<CodeGroup>
```ts TypeScript
const upload = await bucket.signedUploadUrl("u/7/report.pdf", {
  contentType: "application/pdf",
  cache: "immutable",
})

await fetch(upload.url, { method: "PUT", headers: upload.headers, body })
```

```python Python
upload = bucket.signed_upload_url("u/7/report.pdf", content_type="application/pdf", cache="immutable")

httpx.put(upload.url, headers=upload.headers, content=body)
```

```go Go
up, err := bucket.SignedUploadURL(ctx, "u/7/report.pdf",
	&blob.SignedUploadURLOptions{
		ContentType: "application/pdf",
		Cache:       blob.CacheImmutable,
	})
// PUT to up.URL with every header in up.Headers
```
</CodeGroup>

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

### On a direct browser upload

<CodeGroup>
```ts TypeScript
// lib/uploads.ts
import { uniquePath, uploadHandler } from "@upstash/blob"

export const uploads = uploadHandler({
  onBeforeUpload: ({ file }) => ({
    path: uniquePath(`uploads/${file.name}`),
    cache: "immutable",
  }),
})
```

```python Python
# app/uploads.py
from upstash_blob import AsyncUploadHandler, BeforeUploadResult, unique_path

uploads = AsyncUploadHandler(
    on_before_upload=lambda args: BeforeUploadResult(
        path=unique_path(f"uploads/{args.file.name}"),
        cache="immutable",
    ),
)
```

```go Go
// uploads.go
uploads, err := blob.NewUploadHandler(blob.UploadHandlerOptions[struct{}]{
	OnBeforeUpload: func(
		a blob.BeforeUploadArgs[struct{}],
	) (blob.BeforeUploadResult, error) {
		return blob.BeforeUploadResult{
			Path:  blob.UniquePath("uploads/" + a.File.Name),
			Cache: blob.CacheImmutable,
		}, nil
	},
})
```
</CodeGroup>

Decided per upload in the before-upload callback on your server and signed into the presigned PUT. See [Authorizing an upload](/blob/uploads/upload-handler#authorizing-an-upload).

---

## Private buckets

| `cache` | Public bucket | Private bucket |
| ------- | ------------- | -------------- |
| unset | `public, max-age=3600` | `private, max-age=3600` |
| `'1m'` | `public, max-age=60` | `private, max-age=60` |
| `'immutable'` | `public, max-age=31536000, immutable` | `private, max-age=31536000, immutable` |
| `'revalidate'` | `public, max-age=0, must-revalidate` | `private, max-age=0, must-revalidate` |
| `'no-store'` | `no-store` | `no-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

<CodeGroup>
```ts TypeScript
// app/api/avatar/route.ts
const blob = await bucket.put(`avatars/${user.id}.png`, file, {
  contentType: "image/png",
  cache: "immutable",
})

await db.users.update(user.id, { avatar: blob.versionedUrl })
```

```python Python
# app/main.py
blob = bucket.put(f"avatars/{user.id}.png", file, content_type="image/png", cache="immutable")

db.users.update(user.id, avatar=blob.versioned_url)
```

```go Go
// avatar.go
b, err := bucket.Put(ctx, "avatars/"+user.ID+".png", file, &blob.PutOptions{
	ContentType: "image/png",
	Cache:       blob.CacheImmutable,
})
if err != nil {
	return err
}

db.UpdateUserAvatar(ctx, user.ID, b.VersionedURL)
```
</CodeGroup>

```tsx
<img src={user.avatar} />
```

The versioned URL is the URL with the etag on the query, so it changes whenever the content does. A stable path stored `immutable` and served through the versioned URL 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.

A private bucket has neither URL.

---

## `no-store` and signed reads

<CodeGroup>
```ts TypeScript
await bucket.put("private/report.pdf", body, {
  contentType: "application/pdf",
  cache: "no-store",
})

const { url, expiresAt } = await bucket.signedReadUrl("private/report.pdf")
```

```python Python
bucket.put("private/report.pdf", body, content_type="application/pdf", cache="no-store")

link = bucket.signed_read_url("private/report.pdf")
```

```go Go
bucket.Put(ctx, "private/report.pdf", body, &blob.PutOptions{
	ContentType: "application/pdf",
	Cache:       blob.CacheNoStore,
})

link, err := bucket.SignedReadURL(ctx, "private/report.pdf", nil)
```
</CodeGroup>

These are two separate mechanisms. The link expires at its returned expiry, 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 [Signed read URLs](/blob/bucket/reading#signed-read-urls) 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](/blob/uploads/constraints#in-the-browser).
