New guides · Transfer Protocols

Implement Resumable Uploads with HTTP Range Requests in 4 Steps

· UploadBeat

Implement Resumable Uploads with HTTP Range Requests in 4 Steps

A user selects a 2 GB video file, watches the progress bar creep to 87 percent, then loses WiFi for ten seconds. When the connection returns, the upload resets to zero. The user closes the tab.

Resumable uploads solve that problem by letting the server and client agree on which bytes already arrived. HTTP Range requests, originally designed for partial downloads, work in reverse for uploads. The mechanism is standardized, lightweight, and compatible with most proxies and CDNs.

Below are four steps to add resumable upload support to your file transfer workflow. Each step includes the HTTP headers involved, a short code pattern, and one common mistake to avoid.

Step one: assign each upload a unique session identifier

Before any bytes move, the client asks the server for a session ID. This identifier ties all subsequent chunks to the same logical file. Without it, the server cannot distinguish a retry from a brand-new upload.

The client sends a POST or PUT request with metadata: filename, total size, MIME type, and an optional checksum. The server responds with a session token, usually a UUID or a signed string, and stores a record that maps the token to zero bytes received so far.

A common mistake is embedding the filename in the URL path and treating that as the identifier. Two users uploading 'report.pdf' at the same time will collide. Always generate a fresh, random session ID on the server and return it in a custom header or JSON body.

Store the session record in fast storage (Redis, Memcached, or a database table with an index on the session ID). Include an expiration timestamp so orphaned sessions clean themselves up after 24 or 48 hours.

Step two: client sends each chunk with a Content-Range header

Once the client holds a session ID, it splits the file into chunks of fixed size, typically 1 to 5 MB. For each chunk, the client sets the Content-Range header to 'bytes start-end/total', where start and end are zero-indexed byte offsets and total is the file size.

For example, a 10 MB file sent in 2 MB pieces produces five requests. The first chunk carries 'Content-Range: bytes 0-2097151/10485760'. The second carries 'bytes 2097152-4194303/10485760', and so on. The session ID travels in a custom header or query parameter.

The server appends each chunk to a temporary file or object keyed by the session ID. After writing, it updates the session record with the new byte count and responds with HTTP 200 or 201. If the chunk overlaps bytes already received, the server can either reject it with 409 Conflict or silently accept and overwrite.

Clients should verify the response before sending the next chunk. A 500 or timeout means the chunk may not have landed; the client will retry that same range in the next request.

Step three: handle interruptions by querying upload progress

When a connection drops, the client does not know how many bytes the server saved. Before resuming, the client sends a HEAD or GET request with the session ID and no body. The server replies with a Range or Content-Range header indicating the byte offset it holds.

For instance, if the server has bytes 0 through 6291455, it returns 'Range: bytes=0-6291455' or 'Content-Range: bytes 0-6291455/10485760'. The client then starts the next chunk at byte 6291456.

Some implementations use a custom header like 'X-Upload-Offset' instead of Range. Either works; consistency matters more than the header name. Document the expected format so client libraries know which header to parse.

A pitfall is caching the progress query response. Proxies or CDNs may cache HEAD responses for minutes. Set 'Cache-Control: no-store' on progress endpoints, or use a POST request with an empty body to bypass GET/HEAD caches.

Step four: finalize the upload and verify integrity

After the last chunk arrives, the client sends a finalization request with the session ID and an optional checksum (MD5, SHA-256, or a rolling hash). The server concatenates all chunks, computes its own checksum, and compares.

If checksums match, the server moves the temporary file to permanent storage, marks the session complete, and returns HTTP 200 with a permanent file URL or ID. If they differ, the server responds with 400 Bad Request and deletes the temporary file.

Clients should not assume the upload succeeded until they receive the finalization response. A missing or 5xx reply means the server may still be processing. Retry the finalization request with exponential backoff, up to three or four attempts.

Once finalized, delete the session record or mark it expired. Keeping stale sessions wastes storage and complicates debugging. A cron job or background worker can sweep expired sessions every hour.

Why Range requests beat custom protocols

Proprietary chunking schemes require client SDKs, server middleware, and documentation that drifts out of sync. HTTP Range requests are already implemented in every major HTTP library and understood by proxies, load balancers, and CDNs.

Range headers also work with existing authentication layers. OAuth tokens, API keys, and session cookies attach to each chunk request without special handling. Custom protocols often force developers to reimplement auth or tunnel tokens in non-standard ways.

Another advantage is observability. Standard HTTP status codes (206 Partial Content, 416 Range Not Satisfiable) and headers appear in access logs and monitoring dashboards. Operations teams can troubleshoot upload failures without learning a new wire format.

The main trade-off is slightly higher overhead per chunk: HTTP headers add a few hundred bytes. For files larger than a few megabytes, that overhead is negligible compared to the payload.

Practical considerations for production deployments

Choose chunk sizes that balance retry cost and request overhead. One-megabyte chunks work well for mobile networks; five-megabyte chunks suit stable broadband. Let clients configure chunk size based on measured latency and packet loss.

Set reasonable limits on session lifetime and total upload size. A 48-hour expiration and a 10 GB cap prevent abuse and simplify capacity planning. Return 413 Payload Too Large if a client exceeds the size limit.

Log session IDs, byte ranges, and timestamps for every chunk. When users report a stuck upload, correlate client-side logs with server-side records to pinpoint whether the issue is network, server disk, or client retry logic.

Test resumption under real network conditions: throttle bandwidth, inject packet loss, and kill connections mid-chunk. Automated tests should verify that a 10 MB upload interrupted at 7 MB resumes at exactly byte 7340032, not byte zero.

Key facts

Can I use Range requests for uploads smaller than 1 MB?

Yes, but the benefit is small. A 500 KB file uploads in one or two seconds on most connections, so interruptions are rare. The session-management overhead may exceed the time saved by resumption.

What happens if two clients send chunks for the same session ID simultaneously?

The server should serialize writes using a lock or atomic append operation keyed to the session ID. Without locking, chunks may interleave and corrupt the file. Return 409 Conflict if a client sends overlapping ranges.

Related reading: About us · What we do