Blog/Resume interrupted media downloads without corrupting files
OverviewAll posts
Tutorial

Resume interrupted media downloads without corrupting files

Handle HTTP range responses, object changes and expired links when resuming a media download. Validate partial bytes before handing the file to your application.

VTornado API team
Covered in this article
Track committed download bytes
Validate before appending
Recover access and finish the handoff
6 min reading time
Published October 13, 2026
VProduct guides by Velys Software

Resume an interrupted media download only when you can establish that the bytes already saved and the bytes about to arrive belong to the same representation. Keep a partial file separate from the final artifact, validate the response before appending, and publish the file to your consumer only after transfer checks pass.

This guide covers the download from a completed job's delivery destination into your application. Restarting that transfer is different from submitting another media-processing job. Tornado's job-status endpoint lets you inspect the existing job and its delivery result; retain its ID throughout recovery.

Keep a transfer record beside the partial file

Create an application-owned record containing the job ID, stable destination reference, partial-file path, saved byte count and representation validator. Record the expected size when known. Keep temporary access URLs out of ordinary logs and analytics.

Write to a temporary path that downstream consumers cannot mistake for a finished asset. Give one worker ownership of that path. If two workers can append to the same file, correct HTTP handling alone will not protect it.

At restart, reconcile the saved offset with the actual file. An offset stored before a write completed can point beyond durable data; an offset saved too late can cause already-written data to be requested again. Your recovery protocol must decide which bytes are committed before constructing the next request.

Validate the HTTP response before appending

HTTP semantics makes range support optional. A single-range 206 response describes its byte interval through Content-Range; 200 can mean the server sent the whole representation. Never append that full response to an existing prefix.

For example, a committed prefix of 400 bytes calls for Range: bytes=400-. Use a previously captured strong ETag with If-Range; a weak ETag is unsuitable. A validator mismatch can produce a full response instead. Keep request representation settings consistent, including content encoding, and avoid client-side decompression that changes byte offsets.

A 416 is not, by itself, proof that your local file is complete. Reconcile identity and size before accepting it. Unknown or contradictory metadata should stop automatic appending.

The policy below intentionally accepts only a narrow case: an unencoded, single-range response with the same quoted strong ETag and a known total size. It rejects some responses a more capable client could handle safely. That conservative boundary makes its behavior inspectable.

Test the append decision independently

This Python function evaluates metadata and the count of bytes already received into a separate temporary segment. It does not fetch a URL or write to a partial file. Header keys supplied to it must be lower case. The ETag is an opaque validator, not a file checksum.

import re


def can_append(offset, expected_total, saved_etag, status, headers, received):
    if not 0 < offset < expected_total or status != 206:
        return False
    # Conservative subset: a nonempty quoted strong ETag.
    if not re.fullmatch(r'"[\x21\x23-\x7e]+"', saved_etag):
        return False
    if headers.get("etag") != saved_etag:
        return False
    if headers.get("content-encoding", "identity").lower() != "identity":
        return False
    match = re.fullmatch(
        r"bytes ([0-9]+)-([0-9]+)/([0-9]+)",
        headers.get("content-range", ""),
    )
    if match is None:
        return False
    start, end, total = map(int, match.groups())
    return (
        start == offset
        and total == expected_total
        and start <= end < total
        and received == end - start + 1
    )


headers = {"etag": '"revision-a"', "content-range": "bytes 4-9/10"}
assert can_append(4, 10, '"revision-a"', 206, headers, 6)
assert not can_append(4, 10, '"revision-a"', 200, headers, 6)
assert not can_append(4, 10, '"revision-a"', 206, headers, 5)
assert not can_append(4, 10, '"revision-b"', 206, headers, 6)

The exact snippet and additional malformed-range, changed-size, weak-validator, encoding and boundary cases were executed locally with synthetic inputs. These checks establish the decision function's behavior. They do not establish range support for a Tornado delivery provider or verify a live download.

A True result means this segment meets the example's append conditions, not that the entire media file is ready. The accepted segment can end before the expected total. Persist the new committed offset and continue only within your transfer policy. A False result means investigate or restart the transfer into a fresh temporary file; it does not mean regenerate the media job.

Handle an expired link as an access problem

A successful earlier request does not guarantee a later range request will be authorized. Amazon S3 checks presigned URL expiry when a request begins; its documentation specifically distinguishes a download continuing over an established connection from a restart after expiry. See S3 presigned URL behavior.

Recover supported access to the recorded object, then verify its identity again before reusing the prefix. A fresh URL is not evidence that it references the same bytes. If the destination or representation changed, start a separate transfer and keep the old partial artifact out of the consumer's path.

For denied requests or an uncertain object reference, follow the expired-link diagnostic guide. Do not add your API key to a storage URL or broaden storage permissions as a retry technique. There is no universal promise that every delivery destination supports the same HTTP features.

Finish with a controlled handoff

After all segments arrive, check the assembled length against the expected total. Where you have an independently trusted checksum, compare it using the appropriate algorithm; a digest calculated only after downloading provides a fingerprint, not an independent integrity reference. Inspect the media before sending it to the next tool.

Move the verified artifact into its final location through your application's commit procedure, then record that the consumer can use it. Keep transfer completion distinct from successful decoding or analysis. The audio handoff guide and video resolution checks cover those later validations.

Test an interruption, a full response to a range request, a changed object and an expired access link in a controlled environment. Record which cases restarted transfer and which resumed it. Count bytes and attempts separately from processing jobs so a delivery retry does not obscure your acquisition history.

Open the Tornado dashboard to inspect one existing completed job. Establish a reliable full-download path first, then add resumption only for destinations whose behavior you have checked. Use the first media workflow if you still need to establish job submission and delivery.