Skip to content

TUS Protocol Compliance

Compliance status against the TUS resumable upload protocol v1.0.0.

Extensions

Extension Status Notes
core ✅ Implemented POST / HEAD / PATCH, offset tracking, version negotiation
creation ✅ Implemented Upload creation via POST with Upload-Length
creation-with-upload ✅ Implemented Initial data in POST body (Content-Type: application/offset+octet-stream)
creation-defer-length ✅ Implemented Upload-Defer-Length: 1 on POST; final length committed on the first PATCH via Upload-Length
termination ✅ Implemented Upload deletion via DELETE
checksum ✅ Implemented Configurable algorithms (sha1, sha256, sha512, md5); server advertises every enabled algorithm in Tus-Checksum-Algorithm
checksum-trailer ✅ Implemented (bundled transport) Upload-Checksum accepted as an HTTP trailer on chunked requests. Trailer parsing is transport-level: the bundled TusHTTPRequestHandler (and the resumable-upload serve CLI) does it and merges the trailer into the headers before dispatch. Advertised only when TusServer(supports_checksum_trailer=True) — set it if your own transport does the same; ASGI/framework adapters don't yet.
expiration ✅ Implemented Upload-Expires in POST / HEAD / PATCH responses (also propagated to final concatenated uploads); periodic server-side cleanup
concatenation ✅ Implemented Upload-Concat: partial and Upload-Concat: final;url1 url2 …; supported by SQLite, S3, GCS, and Azure backends
concatenation-unfinished ✅ Implemented (SQLite) POST Upload-Concat: final;… while partials are still in progress creates a pending final (201, no Upload-Offset/Upload-Length); assembly + on_upload_complete fire when the last partial completes. Advertised in Tus-Extension only when the storage backend sets supports_unfinished_concat (SQLite does; cloud backends not yet). PATCH on a pending final → 403.

Version Negotiation

Requirement Status
Client sends Tus-Resumable on all non-OPTIONS requests
Server returns Tus-Resumable on all responses
Server returns 412 on version mismatch
Server skips version check for OPTIONS
Server advertises supported versions in Tus-Version (OPTIONS)

Core Protocol — Server

Requirement Status Notes
POST creates new upload, returns 201 + Location
POST returns 400 on missing/invalid Upload-Length
POST returns 400 on negative Upload-Length
POST returns 413 when upload exceeds Tus-Max-Size
POST returns 413 when an individual PATCH exceeds max_chunk_size Server-only knob
HEAD returns 200 with Upload-Offset + Upload-Length Upload-Length omitted for deferred-length uploads until committed
HEAD includes Cache-Control: no-store
HEAD returns 404 for unknown upload
PATCH appends data, returns 204 + updated Upload-Offset
PATCH returns 415 on wrong Content-Type Must be application/offset+octet-stream
PATCH returns 409 on Upload-Offset mismatch
PATCH returns 400 on negative Upload-Offset
PATCH returns 400 if chunk would exceed Upload-Length
PATCH returns 460 on checksum mismatch Non-standard but widely used
PATCH returns 400 on unsupported Upload-Checksum algorithm Algorithm must be in checksum_algorithms
PATCH returns 403 on already-completed upload
PATCH returns 410 on expired upload
OPTIONS returns 204 with server capabilities
OPTIONS includes Tus-Checksum-Algorithm Lists every enabled algorithm
DELETE removes upload, returns 204
DELETE returns 404 for unknown upload
Upload-Concat: partial creates a partial upload Partials never fire on_upload_complete individually
HEAD on a partial echoes Upload-Concat: partial Lets a conformant client distinguish a partial from a normal upload
Upload-Concat: final;… merges partials into a final upload Returns 400 if any partial is missing or incomplete
HEAD on a final echoes Upload-Concat: final;… Source partial IDs are persisted on the final upload (concat_partial_ids); HEAD reconstructs final;<relative urls> in original order. Supported by SQLite, S3, GCS, and Azure backends.
Concurrent PATCH/DELETE serialized via LockBackend Optional; 423 Locked on contention beyond lock_wait_seconds
Malformed Content-Length header → 400
Negative Content-Length400
Upload-Metadata larger than 4 KB → 400 DoS protection
Invalid base64 in Upload-Metadata400
Duplicate Upload-Metadata key → 400 Spec: keys MUST be unique within the header
Non-ASCII Upload-Metadata key → 400 Spec: keys MUST be ASCII. Bare keys (empty value, SP optional) accepted
Socket read timeout (Slowloris protection) TusHTTPRequestHandler.setup() applies request_timeout (default 30s)

Core Protocol — Client

Requirement Status Notes
Sends Tus-Resumable: 1.0.0 on all requests
POST to create upload with Upload-Length
POST to create deferred-length upload (Upload-Defer-Length: 1) create_deferred_upload()
HEAD to get current offset before resuming
PATCH with Upload-Offset and correct Content-Type
Content-Length: 0 in DELETE request
Configurable timeout on all urlopen() calls Default 30s
Catches URLError (network-level) alongside HTTPError
Exponential backoff with cap (max 60s)
Custom retry gating via on_should_retry Domain-specific abort
Upload-Checksum (configurable algorithm) sha1 default; sha256, sha512, md5 opt-in
Cross-session URL persistence (fingerprint-based) FileURLStorage / SQLiteURLStorage / InMemoryURLStorage
Full-file fingerprint (not just first 64 KB) SHA-256 of entire content (default; PartialMD5Fingerprint and CallableFingerprint available)
409 on concurrent offset conflict (atomic CAS) UPDATE ... WHERE offset = ?; returns 409 if row not updated
409 received → HEAD re-sync before retry Client fetches current offset and re-seeks before retrying chunk
Parallel concatenation upload parallel_uploads=N on upload_file()
Manual partial / final concatenation primitives create_partial_upload() / create_final_upload()

Non-standard but supported

Feature Notes
X-HTTP-Method-Override POST rewrites to PATCH/DELETE/HEAD for environments that block those methods
423 Locked on lock contention Returned when lock_backend is configured and the wait timeout elapses
GET download endpoint tusd-style download of completed uploads. Opt-in: TusServer(enable_downloads=True) / resumable-upload serve --enable-downloads. Always Content-Disposition: attachment (anti-XSS); Content-Type from validated metadata filetype, else application/octet-stream. Incomplete → 404, expired → 410. GET is exempt from the Tus-Resumable check (browsers don't send it).

Not Implemented

Feature Notes
Multiple TUS version support Only 1.0.0 supported
tus2 / IETF RUFH (draft-ietf-httpbis-resumable-upload) The standards-track successor protocol (not wire-compatible with 1.0.0). Still a moving draft (draft-11, breaking changes between revisions). Planned as an opt-in experimental protocol flag once the draft stabilizes, mirroring tus-js-client's ietf-draft-NN approach.

Ecosystem Comparison

Feature-by-feature comparison against the most mature official implementations: tusd (Go, the reference server) and tus-js-client (the reference client). Verified against tusd docs/flags and tus-js-client docs/api.md as of 2026-07.

Server: tusd vs resumable-upload

Area tusd (official Go server) resumable-upload
TUS extensions advertised ✅ 5: creation, creation-with-upload, creation-defer-length, termination, concatenation · ❌ checksum / expiration / concatenation-unfinished ✅ 9: those 5 + checksum, checksum-trailer, expiration, concatenation-unfinished
Checksum verification ❌ none (no Upload-Checksum support at all) ✅ sha1/sha256/sha512/md5, header or trailer, 460 on mismatch
Expiration ❌ no TTL/cleanup in the binary (external cleanup required) Upload-Expires + periodic server-side cleanup, 410 on expired
Concatenation over unfinished partials ✅ (SQLite backend)
Storage backends ✅ local disk · ✅ S3 (S3-compatible endpoint, transfer acceleration, part-size tuning, R2 quirks) · ✅ GCS · ✅ Azure (access tiers) ✅ SQLite (zero-dep default) · ✅ S3 · ✅ GCS · ✅ Azure (via extras) · ❌ deep S3 tuning (no acceleration / part-size flags)
Locking ✅ file locker / in-memory · ✅ cooperative lock hand-off · ❌ distributed locker (sticky sessions required to scale out) ✅ in-memory · ✅ Redis (distributed), 423 on contention · ✅ atomic offset CAS → 409 on concurrent PATCH
Hooks: mechanisms ✅ in-process (Go pkg) · ✅ out-of-process: file / HTTP / gRPC / plugin ✅ in-process Python callables · ❌ out-of-process (embedded-library trade-off)
Hooks: events ✅ 7: pre-create, post-create, post-receive (progress, ~1s interval), pre-finish, post-finish, pre-terminate (veto), post-terminate ✅ 6: on_incoming_request (reject), on_upload_create (reject / replace metadata), on_chunk_received (per-chunk progress + stop), on_upload_complete (custom finishing response), on_before_terminate (veto), on_upload_terminate · ❌ interval-based post-receive (per-chunk instead)
Hooks: powers ✅ reject create/terminate · ✅ StopUpload mid-flight · ✅ override upload ID & storage path · ✅ custom HTTP response (pre-create/pre-finish/post-receive) ✅ reject via TusHookError(status) · ✅ replace metadata on create · ✅ stop mid-flight (raise in on_chunk_received → upload deleted) · ✅ custom finishing response (dict from on_upload_complete) · ✅ out-of-band terminate_upload() · ❌ custom upload IDs / storage paths
Download endpoint ✅ GET, on by default (-disable-download to off) · ✅ filetype → Content-Type · ❌ Content-Disposition control ✅ GET, opt-in (enable_downloads=True / --enable-downloads) · ✅ validated Content-Type · ✅ always-attachment disposition (anti-XSS), sanitized filename
CORS ✅ on by default (-disable-cors to off) · ✅ regex origin · ✅ credentials · ✅ extra allow/expose headers · ✅ max-age ✅ opt-in: static string (legacy) or origin list with echo + Vary: Origin · ✅ credentials (cors_allow_credentials, wildcard auto-replaced by echoed origin) · ✅ cors_max_age on preflight · ❌ regex origins · ❌ extra-header knobs
Metrics ✅ Prometheus /metrics · ✅ pprof profiling ✅ Prometheus /metrics (zero-dep registry) · ❌ pprof
Proxy support -behind-proxy honors X-Forwarded-* for absolute Location · ✅ RFC 7239 Forwarded behind_proxy=True honors X-Forwarded-Proto/Host (Host fallback) for absolute Location · ✅ location_base_url for a fixed prefix (relative by default) · ❌ RFC 7239 Forwarded parsing
Networking / TLS ✅ UNIX socket · ✅ HTTP/2 + h2c · ✅ TLS 1.2/1.3 modes · ✅ network + request-completion timeouts ✅ stdlib http.server (CLI) / any ASGI server · ✅ Slowloris socket timeout · ❌ built-in TLS (terminate at your reverse proxy or ASGI server)
Size limits -max-size · ❌ per-chunk cap max_size · ✅ per-PATCH max_chunk_size
Feature toggles -disable-termination · ✅ -disable-concatenation · ✅ -disable-download disable_termination · ✅ disable_concatenation · ✅ downloads opt-in rather than opt-out
Graceful shutdown ✅ SIGINT/SIGTERM drain with -shutdown-timeout ❌ (CLI exits immediately)
Structured logging / request IDs -log-format json · ✅ X-Request-ID surfaced in logs ❌ Python logging only
tus2 / IETF RUFH ✅ experimental (-enable-experimental-protocol) ❌ (tracked; waiting for draft to stabilize)
Deployment model ✅ standalone binary · ✅ usable as Go package ✅ embeddable Python library (zero-dep core) · ✅ CLI · ✅ ASGI adapter · ✅ Flask/FastAPI/Django examples

Summary: ahead of tusd on protocol surface (checksum, expiration, unfinished concat) and distributed locking; behind on operational depth (hook system, proxy/TLS/networking, CORS, graceful shutdown, S3 tuning).

Client: tus-js-client vs resumable-upload client

Area tus-js-client (official JS client) resumable-upload client
Inputs ✅ File/Blob (browser) · ✅ Buffer/Readable (node) · ✅ Cordova file · ✅ React Native URI file_path · ✅ any file-like file_stream (sync); same for httpx-based async client
Chunking chunkSize (default Infinity: whole file in one PATCH) chunk_size (default 1 MiB)
Checksum ❌ not implemented (explicitly out of scope per FAQ) Upload-Checksum per chunk, sha1 default, sha256/sha512/md5 opt-in
Retry retryDelays array [0,1s,3s,5s] · ✅ onShouldRetry override max_retries + exponential backoff (cap 60s) · ✅ on_should_retry override · ✅ stop_event interrupts waits
Resume across sessions ✅ fingerprint → urlStorage (localStorage, default on) · ✅ findPreviousUploads() / resumeFromPreviousUpload() ✅ fingerprint → URL storage (File/SQLite/Memory backends, default off via store_url) · ✅ find_previous_uploads() / resume_upload()
Fingerprint strength ✅ pluggable · ❌ weak default (name/size-based) ✅ full-file SHA-256 default (collision-proof, costlier) · ✅ partial-MD5 and callable alternatives
Parallel upload (concatenation) parallelUploads=N · ✅ custom parallelUploadBoundaries · ✅ metadataForPartialUploads parallel_uploads=N · ✅ metadata_for_partial_uploads · ❌ custom boundaries (even split only, deliberately skipped)
creation-with-upload uploadDataDuringCreation initial_data on create
defer-length uploadLengthDeferred create_deferred_upload()
Termination abort(true) / static terminate() delete_upload()
Pause / partial stop abort() (resume later) · ❌ stop-at-byte stop_event (interrupt-safe) · ✅ stop_at byte offset
Request lifecycle hooks onBeforeRequest · ✅ onAfterResponse · ✅ onUploadUrlAvailable before_request · ✅ after_response · ✅ on_upload_url_available
Progress reporting onProgress(bytesSent,total) · ✅ onChunkComplete · ❌ speed/ETA stats progress_callback(UploadStats) incl. speed, ETA, chunks completed
X-HTTP-Method-Override overridePatchMethod override_patch_method (sync + async)
Request IDs addRequestId (X-Request-ID) add_request_id (UUID per request; user header wins)
TLS control ❌ n/a in browser verify_tls_cert · ✅ mTLS client certificates
Async ✅ Promise-based ✅ separate AsyncTusClient (httpx, [async] extra)
tus2 / IETF RUFH ✅ experimental protocol: 'ietf-draft-03'/'ietf-draft-05' ❌ (tracked)

Summary: ahead on integrity (checksum), fingerprint strength, TLS/mTLS, and stats-rich progress; behind only on RUFH experimentation and custom parallel boundaries.

Error Response Reference

Status Meaning Trigger
400 Bad Request Missing/invalid header, negative offset, chunk overflow, oversized metadata, unsupported checksum algorithm, malformed Upload-Concat, partial referenced by a final that isn't complete
403 Forbidden PATCH on already completed upload
404 Not Found Unknown upload ID
409 Conflict Upload-Offset mismatch or concurrent write conflict
410 Gone Upload has expired
412 Precondition Failed Unsupported TUS version
413 Payload Too Large Exceeds Tus-Max-Size or max_chunk_size
415 Unsupported Media Type Wrong Content-Type in PATCH
423 Locked LockBackend contention; another holder still owns the upload
460 Checksum Mismatch Configured-algorithm digest verification failed

References