Uploading
Every route and limit below is in the reference.
Three steps, and the bytes never touch your Worker — letting users upload files walks the browser side. Images and video want media instead, which wraps this.
1. Start it. Post the logical path and the size. You get back one presigned URL, or — past the multipart threshold — a URL per part.
2. Upload. The browser sends the bytes straight to your bucket.
3. Complete it. Post to the complete route, which confirms the bytes against the store rather than trusting the client’s word that it finished.
That third step is why an upload has a pending state at all. An object that never completes stays pending, holds its quota reservation, and is reclaimed by the daily sweep after its lifetime.
Big files resume
Past the threshold it is a multipart upload, and a dropped connection is recoverable:
GET /storage/:id/partsReturns the parts already stored, plus a fresh URL for each missing one. So a resume is a partial re-upload rather than starting over — which at a few hundred megabytes is the difference between an annoyance and a lost afternoon.
POST /storage/:id/abort abandons one and drops its parts.
Downloads stream through the Worker, on purpose
An upload is presigned and a download is not, and that asymmetry is deliberate: a download is the moment authorization is decided, and a presigned URL is a URL anybody who has it can use.
So the bytes stream through, with range requests, entity tags and disposition handled for you.
GET /storage/:id/url mints a short-lived presigned direct URL where you genuinely want one — for a media player, say. It is minutes, not hours.
Uploaded bytes are treated as untrusted on the way out
This is the part worth reading even if you skim the rest.
A user’s file served from your origin is a script running on your origin, if a browser can be talked into executing it.
So every object response carries no-sniff and a locked-down content policy, and an active type — HTML, SVG, anything ending in an XML suffix, script — is served as an attachment whatever was stored.
Not blocked at upload, which is a different and worse trade: you would be refusing files people legitimately want to keep. Served in a way a browser will not execute.
Keys are opaque, paths are yours
You supply a logical path, and it is stored, indexed and listable — so GET /storage?prefix=… gives you a cursor-paginated listing.
The store key itself is server-derived and opaque. A client can never name an object or guess the one beside it.
Two properties fall out of that: enumeration is impossible, and renaming is a metadata change rather than a copy.
Shares
POST /storage/:id/sharesMints a revocable link at its own short path, because the whole URL gets pasted into a chat window.
The token is the credential — that route is public by design. Revoking is a row you delete, and it takes effect immediately.
An expired share answers a specific code rather than a generic not-found, so a recipient can be told this link expired rather than this does not exist.
Quotas
Enforced per owner, in bytes, rather than per bucket.
An upload that would exceed it is refused at the start — before any bytes move — with a specific code.
Pending uploads hold their reservation, which is what stops somebody starting a hundred uploads to claim space they never use. The sweep reclaims abandoned ones.
Ownership
Objects belong to an authenticated owner, and every owner-scoped route reads the identity off the shared seam.
Without auth composed, those routes deny. That is a defined behavior rather than a crash, and it is also why storage is rarely composed alone.
The daily sweep
Reconciles in both directions: a row with no bytes behind it, and bytes with no row in front.
Both happen — an interrupted delete, a failed complete, an abandoned multipart — and neither is visible to anything else. A store nothing reconciles accumulates orphans forever, and the ones that cost you are the bytes.
Server-side copy
POST /storage/:id/copyDuplicates without the bytes leaving the account. Useful for a template, a snapshot or a fork of somebody’s document.
The error codes worth handling
| Code | Status | Means |
|---|---|---|
storage/not_found | 404 | No such object, or one you may not see |
storage/forbidden | 403 | A public object you do not own |
storage/quota_exceeded | 413 | The upload would put the owner over their limit |
storage/upload_incomplete | 409 | Still pending — the upload never finished |
storage/share_expired | 410 | The token is past its expiry |
That first one is deliberate: an object you may not see answers not found rather than forbidden, because a 403 confirms the object exists.