# Files not caching (cache-control)

> How Storage sets the cache-control header, why browsers re-download files or keep showing old versions, and how to cache well on SupaHost, where files come straight from your instance.

Source: https://supahost.app/kb/storage-cache-control · Updated 2026-11-11

Storage sends a `cache-control` header with every file, and browsers follow it. The value comes from the `cacheControl` option at upload time. Most caching problems come from that value, from signed URLs that change on every call, or from overwriting a file in place.

## What you see

One of two opposite problems:

- **Files download again on every page view**, so pages feel slow and egress climbs.
- **Users keep seeing the old version** of an avatar or document after you replaced it.

Check what the server sends:

```bash
curl -sI "https://app-one.acme.supahost.space/storage/v1/object/public/avatars/u_81/avatar.png" | grep -i -E "cache-control|etag|last-modified"
```

```
cache-control: max-age=3600
etag: "..."
last-modified: ...
```

## Why it happens

- **The default is one hour.** If you don't pass `cacheControl`, Storage stores `3600` seconds, and browsers check again after an hour.
- **Signed URLs contain a token** that's different every time you call `createSignedUrl()`. To the browser each URL is a new file, so nothing is reused.
- **`download()` returns a Blob through JavaScript.** Your code fetches the bytes again each time, unless you keep them yourself.
- **Overwriting with `upsert: true` keeps the same URL.** Browsers that cached the old file keep showing it until `max-age` runs out.

## How to fix it

1. **Set a longer cache time when you upload files that don't change**, such as product photos with unique names:

   ```js
   await supabase.storage
     .from('menu-images')
     .upload(`dishes/${dishId}-${Date.now()}.webp`, blob, {
       cacheControl: '31536000', // one year, in seconds
       contentType: 'image/webp',
     })
   ```

2. **Use a new path for every new version** instead of overwriting. Save the current path in your table:

   ```js
   const path = `u_${userId}/avatar-${Date.now()}.png`
   await supabase.storage.from('avatars').upload(path, file, { cacheControl: '31536000' })
   await supabase.from('profiles').update({ avatar_path: path }).eq('id', userId)
   ```

   Delete the previous file afterwards so it doesn't count toward storage usage. If you must keep a fixed path, add a version to the URL instead: `${publicUrl}?v=${updatedAt}`.

3. **Reuse signed URLs until they expire.** Create one with a sensible lifetime, keep it in memory or your state store, and only request a new one when it's about to run out:

   ```js
   const { data } = await supabase.storage.from('invoices').createSignedUrl(path, 60 * 60)
   ```

4. **Use public buckets for public assets.** Public URLs are stable, so browsers and any CDN you put in front can cache them.

5. **Re-upload to change the header** on a file that already exists. Uploading again with `upsert: true` and a new `cacheControl` replaces both.

## On SupaHost

There's no CDN in front of Storage by default. Files are served from your instance's disk through its gateway, so every uncached download is a request to your instance and counts toward your monthly egress. Good cache headers make pages faster and lower egress.

If you put your own CDN or Cloudflare in front of an app domain, it follows these same headers. For custom domains on SupaHost the Cloudflare record must stay "DNS only".

## Related

- [Serving and downloading files](/docs/storage/serving-files)
- [Egress on SupaHost explained](/kb/egress-explained)
- [Private files still accessible after logout](/kb/private-files-still-accessible)
- [Uploading files](/docs/storage/uploads)
