tus resumable uploads
Large uploads — anything over ~100MB — fail all the time. A dropped connection at 90%, a proxy timeout, a memory limit on the server, a browser that gives up after 10 minutes. Plain form data can’t recover from any of these: the whole upload starts over from zero.
There’s also a hard ceiling you can’t configure away: Cloudflare’s proxy caps request bodies at 100MB (200MB on Enterprise). Behind a Cloudflare proxy, a single request over that limit is rejected before it reaches your server — no matter how the server is configured. The only way around it is to never send the whole file in one request.
tus solves both problems. The tus protocol v1
at /uploads breaks a big file into chunks and tracks the progress in
SQLite. Each PATCH is a small request — easily under the proxy limit —
so a 1GB file uploads as thousands of small, proxy-safe requests. When
a connection drops, the client asks the server where it stopped and
continues from there — no restart, no lost progress.
If your uploads are small (under ~100MB) and the connection is reliable, use form data instead — it’s simpler and has no protocol overhead. tus is for when big files are the point.
Protocol coverage
Section titled “Protocol coverage”Core protocol + five extensions, all spec-compliant:
- Core: OPTIONS (capabilities), POST (creation → 201 +
Location), HEAD (Upload-Offset/Upload-Length), PATCH (application/offset+octet-stream, 415/409/413 error codes) - creation / creation-with-upload:
Upload-Metadata(base64 key-value pairs), initial chunk on POST - termination: DELETE → 204, file + row removed
- expiration:
Upload-Expires+ background sweep - checksum:
Upload-Checksum(sha1/256/384/512/md5, 460 on mismatch) X-HTTP-Method-Overridehook for clients without PATCH/DELETE
Protocol coverage
Section titled “Protocol coverage”Core protocol + five extensions, all spec-compliant:
- Core: OPTIONS (capabilities), POST (creation → 201 +
Location), HEAD (Upload-Offset/Upload-Length), PATCH (application/offset+octet-stream, 415/409/413 error codes) - creation / creation-with-upload:
Upload-Metadata(base64 key-value pairs), initial chunk on POST - termination: DELETE → 204, file + row removed
- expiration:
Upload-Expires+ background sweep - checksum:
Upload-Checksum(sha1/256/384/512/md5, 460 on mismatch) X-HTTP-Method-Overridehook for clients without PATCH/DELETE
Concurrency & safety
Section titled “Concurrency & safety”- Auth + ownership on every endpoint: the session cookie is required (401) and uploads belong to their creator (404 otherwise).
- Atomic offset advancement:
UPDATE ... WHERE offset = ?— concurrent PATCHes race safely, losers get 409 and re-sync via HEAD. - Unguessable ids: 128-bit random base64url ids; files are effectively private by entropy.
- Stored-XSS guard:
/uploadsresponses carryscript-src 'none'in the CSP, and avatars are restricted to raster image types (no SVG).
Client (Profile avatar)
Section titled “Client (Profile avatar)”The client implements the correct resumable pattern:
- POST creation with
Upload-Length+Upload-Metadata - HEAD to reconcile the offset (resume after interruption)
- Chunked PATCH (256KB) with progress
- Link the completed upload as the avatar;
router.reload()refreshes shared props
Interrupted uploads are remembered in localStorage and resumed when the
same file is re-selected.
Config
Section titled “Config”| Variable | Default | Notes |
|---|---|---|
UPLOAD_DIR |
./data/uploads |
upload bytes on disk |
TUS_MAX_SIZE |
0 |
max upload size in bytes (0 = unlimited) |
TUS_EXPIRATION_SECONDS |
0 |
unfinished upload TTL (0 = no expiry) |