Skip to content

Private Storage signed URLs revalidate instead of hitting Smart CDN cache #1186

Description

@matiasvillaverde

Hi Supabase team. I am trying to understand whether this is expected behavior or a Smart CDN issue.

I am serving images from a private Storage bucket with signed URLs. The same exact signed URL is reused across requests. Based on the Smart CDN docs, I expected the first request for that signed URL to be a MISS, and later requests for the same exact URL/token to become HITs.

What I see instead is:

GET 1: cf-cache-status: MISS
GET 2: cf-cache-status: REVALIDATED
GET 3: cf-cache-status: REVALIDATED
GET 4: cf-cache-status: REVALIDATED

The important detail is that I am not generating a new signed URL each time. It is the same URL, same token, same object.

I also tried uploading the object with a very aggressive cache control value:

Cache-Control: public, s-maxage=31536000, max-age=31536000, immutable

The object metadata stores that value correctly, but the signed private URL still revalidates on every request. A HEAD request to the signed URL returns cache-control: no-cache.

Here is the shape of the repro I used, with the actual project URL/token omitted:

# Upload an image to a private bucket with immutable cache metadata.
curl -X POST \
  "$SUPABASE_URL/storage/v1/object/my-private-bucket/test.avif" \
  -H "Authorization: Bearer $SERVICE_ROLE_KEY" \
  -H "apikey: $SERVICE_ROLE_KEY" \
  -H "Content-Type: image/avif" \
  -H "Cache-Control: public, s-maxage=31536000, max-age=31536000, immutable" \
  -H "x-upsert: false" \
  --data-binary @test.avif

# Create one signed URL with a long expiry.
curl -X POST \
  "$SUPABASE_URL/storage/v1/object/sign/my-private-bucket/test.avif" \
  -H "Authorization: Bearer $SERVICE_ROLE_KEY" \
  -H "apikey: $SERVICE_ROLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expiresIn":1576800000}'

# Then reuse the returned signedURL exactly.
for i in 1 2 3 4; do
  curl -sS -o /dev/null -D - "$SIGNED_URL" \
    -w "attempt=$i ttfb=%{time_starttransfer} total=%{time_total}\n" \
    | grep -Ei '^(HTTP/|cf-cache-status:|age:|cache-control:|etag:|expires:|last-modified:|attempt=)'
done

Observed headers on repeated GETs look like this:

HTTP/2 200
cf-cache-status: REVALIDATED
etag: "..."
expires: <far future date based on signed URL expiry>
last-modified: <object timestamp>

A HEAD request to the same signed URL returns:

HTTP/2 200
cf-cache-status: MISS
cache-control: no-cache
etag: "..."
expires: <far future date based on signed URL expiry>
last-modified: <object timestamp>

For comparison, a public bucket object on the same project behaves as expected:

cf-cache-status: HIT
age: <increasing>
cache-control: 31536000

So the CDN is working generally; the behavior seems specific to private signed object URLs.

I found this older issue, which says signed URLs missing cache was expected at the time: supabase/supabase#23406

But the current Smart CDN docs now say signed URL responses are cached at the edge and that subsequent requests using the exact same signed URL should receive a cache hit: https://supabase.com/docs/guides/storage/cdn/smart-cdn#signed-urls-and-cdn-caching

Is REVALIDATED on every request expected for private signed URLs, even when the same signed URL is reused and the object has immutable cache metadata? Or should these eventually become HIT responses?

If there is any extra metadata or signed URL option needed for immutable assets that never change, I would be happy to try it. I could not find a documented way to override Cache-Control on the signed URL response itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions