Streaming responses and uploads

HTTPX · Advanced · 11 min

Downloads larger than memory, progress bars and server-sent events all need streaming.

Normally HTTPX reads the whole response body into memory. For large downloads or long-lived streams, use client.stream(method, url) as a context manager:

with client.stream("GET", url) as r:
    for chunk in r.iter_bytes():
        ...

Inside the block the headers are available immediately, and you choose how to consume the body:

  • iter_bytes() decoded bytes (gzip and other content encodings removed),
  • iter_text() decoded text, iter_lines() line by line (great for NDJSON and server-sent events),
  • iter_raw() the raw bytes exactly as sent over the wire, still compressed.

For async clients use async with client.stream(...) and the aiter_bytes(), aiter_text(), aiter_lines(), aiter_raw() versions.

The connection is released when the block exits, even if you stop reading early. Outside the block the body is gone: accessing r.content on a streamed response that you did not read raises httpx.ResponseNotRead. If you decide mid-way that you want the whole body, call r.read() (or await r.aread()).

r.num_bytes_downloaded tracks progress through the raw stream, which, combined with Content-Length, drives a progress bar.

Request bodies can be streamed too: pass a generator of bytes (or an async generator for AsyncClient) as content=. HTTPX sends it with chunked transfer encoding, so the data never has to be in memory at once. Some servers reject chunked uploads; if you know the size in advance, set a Content-Length header and HTTPX sends the stream without chunking.

Example

import httpx

with httpx.Client(base_url="https://httpbin.org") as client:
    with client.stream("GET", "/bytes/50000") as r:
        total = int(r.headers["Content-Length"])
        received = 0
        for chunk in r.iter_bytes(chunk_size=8192):
            received += len(chunk)
        print(received == total, r.num_bytes_downloaded)  # True 50000

    with client.stream("GET", "/stream/3") as r:      # newline-delimited JSON
        print(sum(1 for line in r.iter_lines() if line))  # 3

    def body():
        for part in (b"chunk1,", b"chunk2"):
            yield part
    r = client.post("/post", content=body(), headers={"Content-Length": "13"})
    print(r.json()["data"])                           # chunk1,chunk2

Remember

  • client.stream(...) as r plus iter_bytes/iter_lines consumes the body incrementally
  • Async variants are aiter_bytes, aiter_lines, ...
  • Generators passed as content= stream the request body

Watch out for

  • Accessing r.content outside the stream block, which raises ResponseNotRead.

Check yourself

  1. Inside client.stream(...), which method yields the bytes as sent on the wire, before gzip decoding?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.python-httpx.org/quickstart/#streaming-responses