python-hyper/h11
> A pure-Python, bring-your-own-I/O implementation of HTTP/1.1 — a protocol state machine with no network code of its own.
GitHub repo · Official website · License: MIT
Overview
h11 is a "sans-I/O" HTTP/1.1 library: it contains no socket code, no async runtime, no threads. You feed it bytes that you read off the network, and it hands you back a list of high-level HTTP events; you hand it events, and it gives you back bytes to write. What actually moves those bytes — asyncio, Trio, threads, a raw blocking socket — is entirely your problem, which is the whole point. It was written by Nathaniel J. Smith (author of Trio) starting in 2016, explicitly inspired by Cory Benfield's hyper-h2 and his argument that every new network API in Python otherwise forces someone to reimplement HTTP from scratch1.
This makes h11 a toolkit, not a product. It is not a drop-in replacement for requests or aiohttp; it does not fetch URLs. It is the layer you build those things on top of. In practice most Python developers use h11 without knowing it: it is the HTTP/1.1 engine underneath encode/httpcore (and therefore httpx), and one of the two parser backends in uvicorn. Its audience is the narrow but load-bearing set of people writing HTTP clients, servers, and proxies.
The defining tension is scope discipline. h11 implements the framing chapter of HTTP/1.1 — RFC 7230: message syntax, chunked transfer encoding, keep-alive, content-length, the connection state machine — and deliberately refuses to know about routing, cookies, caching, content negotiation, or TLS2. That minimalism is why it is roughly 800 lines you can read in an hour, and also why it does nothing useful on its own.
Getting Started
pip install h11
import h11, socket
# Minimal blocking client — h11 does framing, the socket does I/O.
sock = socket.create_connection(("httpbin.org", 80))
conn = h11.Connection(our_role=h11.CLIENT)
# 1. Turn events into bytes and send them yourself.
sock.sendall(conn.send(h11.Request(
method="GET", target="/get",
headers=[("Host", "httpbin.org"), ("Connection", "close")],
)))
sock.sendall(conn.send(h11.EndOfMessage()))
# 2. Feed received bytes in; pull high-level events out.
while True:
event = conn.next_event()
if event is h11.NEED_DATA:
conn.receive_data(sock.recv(4096))
elif isinstance(event, h11.Response):
print(event.status_code, dict(event.headers))
elif isinstance(event, h11.Data):
print(event.data.decode(errors="replace"))
elif isinstance(event, h11.EndOfMessage):
break
The API is symmetric: the events a client sends (Request, Data, EndOfMessage) are exactly the ones a server receives, and vice-versa. Swap our_role=h11.SERVER and the same object drives the other side.
Architecture / How It Works
The core is a single Connection object wrapping two coupled state machines — one tracking your role, one tracking the peer — plus a byte-level parser. There is no callback registration and no inheritance-based framework; you drive it by calling send(), receive_data(), and next_event() in a loop.
Events are the vocabulary: Request, InformationalResponse (1xx), Response, Data, EndOfMessage, and ConnectionClosed. Two sentinels, NEED_DATA and PAUSED, signal flow-control states rather than protocol events. Any peer violation raises h11.RemoteProtocolError; a local misuse raises h11.LocalProtocolError. Both subclass h11.ProtocolError, so the state machine, not your ad-hoc if statements, enforces legality — you cannot, for example, send a body before a request line.
The parser is pure Python and was a deliberate rewrite: early versions wrapped Node's C http-parser via Cython plus a yield from state machine, and both were removed because the pure-Python parser needed fewer lines, used no exotic syntax, and had more features2. The design constraint that survives is algorithmic: parsing is linear-time and bounded-memory even against pathological input (slowloris-style dribbling, unbounded header lines), with no byte-by-byte Python loops on the hot path. It handles the genuinely fiddly parts of HTTP/1.1 that naive parsers get wrong — chunked framing, keep-alive differences across 1.0/1.1, and obsolete line folding.
Because h11 owns no I/O, back-pressure and timeouts are entirely the caller's responsibility. This is the load-bearing coupling story: h11 tells you what the bytes mean and when it needs more, but never blocks, never sleeps, and never touches a socket.
Production Notes
- You will not use h11 directly in most apps — and shouldn't. If you want an
HTTP client, use httpx (which uses httpcore, which uses h11). Reach for h11 only when you are building the transport layer itself: a custom async server, a proxy, or an HTTP client for a runtime that lacks one.
- CVE-2025-43859 (fixed in 0.16.0). Versions before 0.16.0 were leniently
parsing chunked-encoding line terminators, enabling request smuggling when h11 sat behind or in front of another HTTP implementation that framed differently. Anyone using h11 in a client or proxy — including transitively via old httpcore/httpx pins — should be on 0.16.0 or later3. This is the single most important upgrade note for the library.
- Sans-I/O means you own the footguns I/O usually hides. Timeouts,
read/write buffering, and connection reuse are yours to implement. h11 will happily return NEED_DATA forever if the peer stalls; a slowloris defense is your loop's job, not h11's.
- Connection reuse requires an explicit reset. After a full request/response
cycle on a keep-alive connection you must call conn.start_next_cycle(); forgetting it is a common source of LocalProtocolError.
- Throughput is fine, not exceptional. The pure-Python parser trades
micro-optimization for simplicity and correctness. For raw parse speed under extreme load, uvicorn's alternative httptools backend (a C parser) is faster; h11 is chosen for portability, auditability, and sans-I/O composition, not benchmark wins. The maintainer has historically declined to publish benchmarks2.
- Stable to the point of quiet. The API has been broadly frozen for years;
releases are infrequent and mostly conformance or security fixes. Treat long gaps between releases as maturity, not abandonment — but do track the security advisories, since those are exactly the releases you must not skip.
When to Use / When Not
Use when:
- You are writing an HTTP/1.1 client, server, or proxy and want to control I/O
yourself (custom async runtime, threads, or exotic transport).
- You want a small, auditable, dependency-free protocol core you can actually
read end to end.
- You need strict RFC 7230 framing behavior with protocol violations surfaced as
exceptions rather than silently mishandled.
Avoid when:
- You just need to make HTTP requests — use httpx or requests; h11 is a layer
too low.
- You need HTTP/2 or HTTP/3 — h11 is 1.1 only; its sibling h2 covers HTTP/2.
- Raw parsing throughput is your bottleneck and you can accept a C dependency —
httptools or a Rust/C parser will be faster.
Alternatives
- python-hyper/h2 — the HTTP/2 sibling with the same sans-I/O design; use it
when you need HTTP/2 framing instead of 1.1.
- encode/httpcore — a minimal HTTP client transport built on h11 (and h2); use
it when you want a ready-made connection pool, not a bare protocol machine.
- MagicStack/httptools — a fast C parser (Node http-parser bindings); use when
you need maximum parse speed and don't need the sans-I/O event model.
- aio-libs/aiohttp — a batteries-included async client/server framework; use
when you want the whole stack, not a protocol toolkit.
- encode/httpx — a full high-level HTTP client; use when you are an application
developer, not a transport author.
History
| Version | Date | Notes |
|---|---|---|
| 0.1.0 | 2016 | Initial release; sans-I/O HTTP/1.1 state machine1. |
| 0.8.0 | 2018 | API maturation; event model stabilized. |
| 0.11.0 | 2020 | Last release supporting Python 2. |
| 0.12.0 | 2021 | Python 3-only; type hints added. |
| 0.14.0 | 2022 | Python 3.8+ baseline; PyPy3 support. |
| 0.16.0 | 2025-04 | Security fix CVE-2025-43859 (chunked-encoding request smuggling)3. |
References
- ^ Cory Benfield, "The New Hyper" — the sans-I/O rationale h11 is built on. https://lukasa.co.uk/2015/10/The_New_Hyper/
- ^ h11 documentation and README, "FAQ" — scope, the pure-Python parser rewrite, and the maintainer's notes on performance. https://h11.readthedocs.io/
- ^ GitHub Security Advisory GHSA-vqfr-h8mv-ghfj / CVE-2025-43859 — h11 accepted malformed chunked-encoding line terminators, enabling request smuggling; fixed in 0.16.0. https://github.com/python-hyper/h11/security/advisories/GHSA-vqfr-h8mv-ghfj
Tags
python, http, http1.1, sans-io, networking, protocol, parser, library, async, mit-license