Guide

Upload links don't survive expiry

Pre-signed PUT URLs are short-lived by design. Here's why, how long you get, and what to do when "the link expired" happens.

5/18/2026 · connect0 · 2 min

Connecting a file into the core is deliberately time-boxed — pre-signed links expire so a leaked capability can't linger.

Every upload.create_link URL has a clock on it. By default 15 minutes; you can ask for up to 1 hour. After that the URL just stops working — R2 returns 403 with no special message.

Why so short

A pre-signed URL is a capability. Anyone who has it can PUT to that key — no further authentication. Short TTLs are how we bound the blast radius:

  • A URL pasted into a Slack DM that gets archived is harmless an hour later.
  • A leaked URL can't be used to backfill historical data.
  • The audit row's expires_at is the upper bound — even if you've kept the URL around, it stops accepting writes.

We don't ship 24-hour links. If you need a long-running upload window, the answer is "session-bound" credentials (which we don't have yet — see the CI auto-upload guide for the roadmap).

What "expired" looks like

HTTP 403  AccessDenied

That's not "the link is wrong" — it's "the link was valid but isn't anymore." Distinguish from a size-mismatch 403 by checking the body: SignatureDoesNotMatch vs AccessDenied or Request has expired.

Browser-helper UX

The /u/[token] page checks the link's expires_at server-side and renders an "Upload link expired" message before you can drop the file. So you get a clear UI signal rather than a mid-PUT failure.

Asking the agent for a fresh link

"The previous link expired. Give me a new one for the same file."

The agent calls upload.create_link again. Same target_path is fine — when you eventually PUT, R2 replaces whatever was (or wasn't) there. The old link row stays in the audit log with claimed_at = NULL so you can see "this was issued and abandoned."

Quietly long uploads

If your PUT is slow (multi-gig over a coffee-shop wifi), starting near the expiry boundary is risky. R2 evaluates the signature at the moment the request is initiated, but if a connection drops and your client retries past the boundary, the retry fails. Pad the TTL: pass expires_in_seconds: 3600 for big files on uncertain links.

Next steps

Where this fits

This is the Connect everything pillar of connect0 — the core that connects everything your company runs on. Short-lived links keep the path for connecting data into the core safe by default. Start building → · All guides →