httpx download file: stream to disk in chunks (tested)

Use with httpx.stream("GET", url, follow_redirects=True) as r:, call r.raise_for_status(), then write r.iter_bytes() chunks to a file opened with "wb". Without follow_redirects a redirecting link saves a 0-byte file.

To download a file with httpx without loading it all into memory, use httpx.stream() (or client.stream()) and write iter_bytes() chunks to a file opened in "wb" mode. Measured with httpx 0.28.1 on Python 3.13.5 against a local http.server (srv.py) serving a 1 MiB /file.bin, a 302 at /r and a 404 at /missing.

1. httpx download file in chunks

import os, httpx
from srv import start
base = start()

with httpx.stream("GET", base + "/file.bin") as r:
    r.raise_for_status()
    total = int(r.headers["Content-Length"])
    with open("file.bin", "wb") as f:
        for chunk in r.iter_bytes(chunk_size=65536):
            f.write(chunk)
    print("downloaded", r.num_bytes_downloaded, "of", total)
print("on disk:", os.path.getsize("file.bin"))

Output:

downloaded 1048576 of 1048576
on disk: 1048576

r.num_bytes_downloaded counts raw bytes received, which is what you compare with Content-Length for a progress bar.

2. Gotcha: redirects are not followed

import os, httpx
from srv import start
base = start()

# without follow_redirects you save the empty 302 body
with httpx.stream("GET", base + "/r") as r:
    with open("a.bin", "wb") as f:
        for chunk in r.iter_bytes():
            f.write(chunk)
print(r.status_code, os.path.getsize("a.bin"), "bytes")

with httpx.stream("GET", base + "/r", follow_redirects=True) as r:
    with open("a.bin", "wb") as f:
        for chunk in r.iter_bytes():
            f.write(chunk)
print(r.status_code, os.path.getsize("a.bin"), "bytes")

Output:

302 0 bytes
200 1048576 bytes

Without follow_redirects=True the loop happily writes the empty 302 body and you get a 0-byte file. Many download links (GitHub releases, CDNs) redirect, so pass it and call raise_for_status() before opening the output file.

3. Reading the error body of a streamed response

import httpx
from srv import start
base = start()

try:
    with httpx.stream("GET", base + "/missing") as r:
        r.raise_for_status()
except httpx.HTTPStatusError as e:
    print(e.response.status_code)
    try:
        print(e.response.text)
    except httpx.ResponseNotRead as e2:
        print("ResponseNotRead:", e2)

Output:

404
ResponseNotRead: Attempted to access streaming response content, without having called `read()`.

Inside stream() the body has not been read, so e.response.text raises ResponseNotRead. Call r.read() before raise_for_status() when the status is an error if you want to log it.

4. AsyncClient download

import asyncio, os, httpx
from srv import start
base = start()

async def main():
    async with httpx.AsyncClient() as client:
        async with client.stream("GET", base + "/file.bin") as r:
            r.raise_for_status()
            with open("async.bin", "wb") as f:
                async for chunk in r.aiter_bytes(65536):
                    f.write(chunk)
    print("on disk:", os.path.getsize("async.bin"))

asyncio.run(main())

Output:

on disk: 1048576

Related: httpx stream and iter_bytes, httpx follow redirects, httpx raise_for_status, httpx upload file.

Frequently asked questions

How do I download a large file with httpx?

with httpx.stream('GET', url) as r: write each chunk from r.iter_bytes() to a file opened in 'wb' mode. Memory use stays at one chunk.

Why is my httpx download 0 bytes?

The URL redirected and httpx does not follow redirects by default; the 302 body is empty. Pass follow_redirects=True.

How do I show download progress with httpx?

Compare r.num_bytes_downloaded with int(r.headers['Content-Length']) inside the iter_bytes() loop.

Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1 on Python 3.13.5 against a local http.server serving a 1 MiB file. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx is free.