Using If-Range to Resume Interrupted Downloads
Problem Statement
A large download is interrupted at 60%. The client should be able to ask for the remaining 40% and append it to what it already holds. That is only safe if the file has not changed in the meantime, and If-Range is the precondition that establishes it. When the header is mishandled the failure is silent: either resumption never happens and every interruption costs a full restart, or it happens when it should not and produces a corrupt file.
Prerequisite Concepts
- Range requests and partial content caching — the
RangeandContent-Rangemechanics resumption is built on. - Strong vs weak ETags explained — the comparison function
If-Rangerequires. - ETag generation and validation strategies — how to produce a validator that survives a deploy.
Step-by-Step Resolution
Step 1 — Capture the validator
Resumption starts with whatever the client recorded on the first attempt:
curl -sI https://example.com/downloads/dataset-2026.tar.gz \
| grep -iE '^etag|^last-modified|accept-ranges|content-length'
Accept-Ranges: bytes
ETag: "9f21c4a8b7"
Last-Modified: Wed, 15 Jul 2026 11:02:00 GMT
Content-Length: 4831838208
A quoted ETag with no W/ prefix is what you want. A weak validator here means every resumption attempt will be refused.
Step 2 — Interrupt a transfer and record the offset
curl -s --max-time 5 -o partial.bin \
https://example.com/downloads/dataset-2026.tar.gz
GOT=$(stat -c%s partial.bin)
echo "have $GOT bytes"
Step 3 — Resume with both headers
curl -s -D - -o remainder.bin \
-H "If-Range: \"9f21c4a8b7\"" \
-H "Range: bytes=$GOT-" \
https://example.com/downloads/dataset-2026.tar.gz | head -5
A 206 with Content-Range: bytes <GOT>-4831838207/4831838208 confirms the server accepted the precondition and is continuing from the offset. Concatenating the two files should reproduce the original exactly.
Step 4 — Test the failure path deliberately
The success path proves the header is read. The failure path proves it is enforced, which is the property that protects against corruption:
curl -s -o /dev/null -w 'status %{http_code} size %{size_download}\n' \
-H 'If-Range: "stale-value-that-cannot-match"' \
-H "Range: bytes=$GOT-" \
https://example.com/downloads/dataset-2026.tar.gz
The expected result is 200 with the full resource size. A 206 here means the precondition is being ignored, and a client resuming after a content change will silently produce a corrupt file.
Step 5 — Make the validator survive a deploy
The most common cause of broken resumption is a validator derived from filesystem metadata. A release rewrites every file’s modification time, so every in-flight download loses its precondition even though the bytes are identical:
# Disable the mtime-derived ETag and serve a content-derived value instead
etag off;
location /downloads/ {
# Value precomputed at build time and stored alongside the artefact
add_header ETag $upstream_http_x_content_hash always;
add_header Accept-Ranges bytes always;
}
# Remove the inode component so the value is identical across servers
FileETag MTime Size
Step 6 — Confirm the client is actually using it
Server-side correctness is only half the picture: many clients never attempt resumption at all. A browser download manager, a package manager, a mobile SDK and a curl invocation all differ in whether they record the validator and whether they send it back.
The quickest check is the access log. Count requests to the download path that carry both a Range and an If-Range header, against those carrying Range alone:
awk '/\/downloads\// && /Range:/ {n++; if ($0 ~ /If-Range:/) c++} END {
printf "%d ranged requests, %d with If-Range (%.0f%%)\n", n, c, 100*c/n }' access.log
A high proportion of bare Range requests means clients are resuming without checking, which is a correctness risk you cannot fix from the server — the only mitigation is to make the artefact immutable under its URL so that resuming without a precondition is always safe. Publishing each build under a versioned path achieves exactly that, and removes the whole class of problem rather than detecting it.
Expected Output / Verification
A correct implementation produces 206 on a matching validator, 200 on a stale one, and a concatenated file whose checksum matches a single uninterrupted download:
cat partial.bin remainder.bin > joined.bin
sha256sum joined.bin
curl -s https://example.com/downloads/dataset-2026.tar.gz | sha256sum
The two digests must be identical. This is the only test that proves the resumption produced correct bytes rather than merely a plausible response.
Edge Cases
- A weak
ETag.If-Rangerequires strong comparison, so a weak validator always fails the precondition and every resumption becomes a restart. Last-Modifiedwith sub-second changes. Two edits within the same second share a timestamp, so a resumption can append bytes from the second version onto a prefix from the first.- A CDN rewriting the validator. If the edge generates its own
ETag, the value the client recorded may not match what the origin compares against, and resumption fails through the CDN while working directly. - Compression applied to the download. Range offsets refer to transferred bytes, so a change in encoding between the two requests makes the recorded offset meaningless.
- A load balancer distributing to servers with different validators. Resumption succeeds or fails depending on which server answers, producing an intermittent failure that is difficult to reproduce.
- An artefact replaced in place during a release. Even a correct precondition then does the right thing — a full restart — but the user experience is poor. Publishing new artefacts under new URLs avoids the conflict entirely.
Frequently Asked Questions
Why not just send Range without If-Range?
Because the resource may have changed since the first part was downloaded. Appending new bytes to an old prefix produces a corrupt file that no error will report. If-Range makes the server check before honouring the range.
What happens if the validator no longer matches?
The server ignores Range and returns 200 with the complete current representation. The client discards its partial copy and starts again — the designed fallback, not an error.
Can Last-Modified be used with If-Range?
Yes, and it uses strong comparison of the date. It is weaker in practice than an ETag because it cannot detect two changes within the same second, which is exactly the window in which a resumption is most likely to be wrong.
Does a deploy break in-flight downloads?
It does if the validator is derived from filesystem metadata, because every file gets a new value whether or not its bytes changed. A content-derived validator survives a deploy of unchanged content and keeps resumption working.
Related
- ETag vs Last-Modified: which validator to use covers the choice that decides whether resumption is reliable.
- Debugging 206 Partial Content cache behaviour isolates the tier when a range never reaches the origin.
- Conditional requests and 304 responses covers the other precondition family and how the origin evaluates them.