# geventhttpclient: an HTTP client for Python that parses headers in C

> geventhttpclient is a gevent-native HTTP client that uses the llhttp parser written in C, ships drop-in replacements for requests, httpx and http.client, and streams bodies instead of buffering them. It is explicitly not a feature-complete replacement for requests, and the README says when not to use it.

**geventhttpclient/geventhttpclient** — A high performance, concurrent http client library for python with gevent

- Repository: https://github.com/geventhttpclient/geventhttpclient
- Stars: 564 · Forks: 136
- Language: Python
- License: NOASSERTION
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/geventhttpclient-geventhttpclient

## The pitch is per-request overhead, not transfer speed

geventhttpclient is described as a fast, gevent-native HTTP client for Python with a low-overhead C parser, and drop-in APIs for requests, httpx and http.client. The example at the top of the README is two lines:

```python
import geventhttpclient as requests

requests.get("https://github.com").text
```

The Why section is careful about what the claim means. HTTP parsing is done by the C-based llhttp parser, which reduces client-side CPU cost. The README adds the important qualifier immediately: this does not make transfers faster, and it points to the benchmarks section for what is actually measured.

That distinction is the whole design. A client that spends a lot of CPU parsing response headers burns CPU that could be serving other greenlets, and in a gevent application that CPU time is concurrency you are not using. The C parser is there to give that time back.

The rest of the six reasons follow from it. A greenlet-safe connection pool with HTTP/1.1 keep-alive, so you can share one client across thousands of greenlets. Streaming first, reading bodies incrementally line by line or in chunks and sending chunked request bodies from generators, so memory stays flat. Drop-in compatible, switching from requests, httpx, http.client, httplib2 or urllib by changing an import or adding one patch line. SSL verification using the certifi CA bundle, the same one requests ships. And modern packaging, Python 3.11 to 3.15 with wheels for common platforms and full type annotation through a py.typed marker.

The project is on PyPI as geventhttpclient, MIT licensed, with the last push on 2026-10-09 and a release that same day, 2.6.1, after 2.6.0 earlier in October and 2.5.1 at the end of September.

## What it is not is stated in a blockquote above the features

Before the features, the README has a blockquote that answers the question a careful adopter asks first. It is not a full replacement for requests or httpx. It covers the common cases, cookies, form data, compressed bodies, proxies, but is not as feature rich, and has no HTTP/2 support.

That is worth reading twice, because the drop-in framing elsewhere in the README could lead you to believe otherwise. The word drop-in refers to the import surface, not to the feature surface.

The missing HTTP/2 is the one that matters most in practice. HTTP/2 multiplexing changes the economics of a client that opens many connections, and a client that is HTTP/1.1 only will open more sockets and do more handshakes than a modern one. If your traffic is to servers that speak HTTP/2 well, that is a real cost.

The other limitation is per-request configuration, which the httpx-compatible section states explicitly: per-request options are not plumbed through, because the engine is configured at session level, and HTTP/2 and mounts are out of scope. So the options that requests lets you vary per call, a proxy for one request, a timeout for one request, a specific set of headers, are set for the whole session instead.

## Two ways in, and one low-level client underneath

The Choose your interface section is a table of what to change if you are migrating, and it is the fastest way to understand the library's structure.

If you currently use requests, you switch to the requests-compatible interface and the effort is changing one import. If you use httpx.Client, you switch to the httpx-compatible interface, also one import. If you use http.client, you switch to geventhttpclient.httplib, again one import. If you use httplib2 or urllib.request, you use the geventhttpclient.httplib.patch() monkey patching, which is one line before the import. And if you want maximum control, you use the native HTTPClient API.

The requests-compatible path covers basic HTTP usage including cookie management, form data and decoding of compressed bodies:

```python
import geventhttpclient as requests

requests.get("https://github.com").text
requests.post("http://httpbingo.org/post", data="asdfasd").json()

from geventhttpclient import Session

s = Session()
s.get("http://httpbingo.org/headers").json()
s.get("https://github.com").content
```

The httpx-compatible path uses the same engine and gevent concurrency, with lower HTTP parsing overhead than the httpx default:

```python
from geventhttpclient import httpx

with httpx.Client() as client:
    response = client.get("https://github.com")
    response.raise_for_status()
    for chunk in response.iter_text(64):
        ...
```

Underneath all of them is HTTPClient, which has a built-in connection pool and is greenlet safe by design, so a single instance can be shared among many greenlets.

## The connection pool is the concurrency model

The quick start shows the low-level client used for concurrency, and the shape of it matters more than the individual calls:

```python
import gevent.pool
from geventhttpclient import HTTPClient, URL

url = URL("https://httpbingo.org/")
client = HTTPClient.from_url(url, concurrency=10)  # pool of up to 10 connections


def fetch(i):
    return client.get(f"/get?i={i}").status_code


pool = gevent.pool.Pool(50)  # more greenlets than connections is fine, they wait for a free one
print(pool.map(fetch, range(100)))
client.close()
```

Two numbers are doing different jobs. The concurrency argument limits the number of connections in the pool, and greenlets beyond that limit wait for a free connection. The gevent pool size is the number of concurrent greenlets, and the comment in the example points out that having more greenlets than connections is fine, since they wait.

That distinction is the whole concurrency story. The pool size is how much work you want in flight; the concurrency setting is how many sockets you are willing to hold open. Setting them the same is the easy mistake, and it is the one that produces a service that opens as many connections as it has concurrent tasks.

The low-level version of the same thing shows the mechanics more directly, and the comment on the request line explains the behaviour:

```python
import gevent.pool
from geventhttpclient import HTTPClient, URL

url = URL("https://httpbingo.org/")
client = HTTPClient.from_url(url, concurrency=10)


def fetch(i):
    response = client.get(f"/delay/1?i={i}")  # blocks until a connection is free
    return response.status_code


pool = gevent.pool.Pool(20)
results = pool.map(fetch, range(100))
client.close()
```

The base client is the same shape, constructed from a host rather than a URL, and it uses the request URI rather than a full URL for the call.

## Streaming is the memory story

Response bodies are read incrementally from the socket, so large responses never have to be held in memory, and the API for it is small.

read(n) returns up to n bytes. readline(sep) returns one line, and you pass a newline byte for line-oriented payloads. The README notes that the default separator is the one that terminate a line, and the chunked form is there for larger reads.

This matters in two directions. On the response side, a large download does not become one string in memory. On the request side, chunked request bodies from generators mean you can post a stream rather than materialising it.

The session example in the quick start shows the ordinary shape, and it is the one most code will use:

```python
from geventhttpclient import Session

with Session() as s:
    r = s.get("https://httpbingo.org/get")
    r.raise_for_status()
    print(r.json())
```

The with block is the point. A Session holds a connection pool, and closing it releases the sockets, so the context manager is the natural way to use it in a test or a short lived script.

The examples directory in the repository carries the same idea further, with download.py and download_raw.py for streaming a body to disk, facebook.py for a real site, oauth2.py for an authenticated flow, and httplib2_patched.py and urllib_patched.py for the monkey patching routes.

## A C extension built from vendored llhttp sources

The setup.py in the repository is short and shows exactly what the native part is. The extension is called geventhttpclient._parser, and it is compiled from four C source files: the project's own ext/_parser.c, plus three from the llhttp tree, llhttp/src/api.c, llhttp/src/http.c and llhttp/src/llhttp.c. The include directories are ext and llhttp/include.

The llhttp parser is vendored as a submodule, and the repository has a llhttp directory at the top level along with a .gitmodules file. The licence section in the pyproject handles the licensing of the vendored code, listing license-files with both LICENSE-MIT and llhttp/LICENSE, and the package classifiers mark the project as MIT while the vendored parser keeps its own terms.

Installation is one command:

```bash
pip install geventhttpclient
```

It requires gevent 25.9 or newer, and the reason is spelled out in a comment in the pyproject: the single floor applies to all supported interpreters and was verified against the full test suite on Python 3.11 to 3.15, because gevent 25.9 is the first release shipping wheels for Python 3.14, and older gevent releases are not supported.

Wheels are provided for common platforms, and otherwise the package builds from source and needs a C compiler. That is the trade for the C parser: on an unsupported platform you need a toolchain.

The runtime dependencies are short: gevent, certifi for the CA bundle, and brotli for compressed responses. The build system uses setuptools with setuptools-scm, so the version is derived from the git history rather than written in the manifest, and the author is Antonin Amand.

## Conclusion

Adopt geventhttpclient if your Python service already uses gevent, you make a high volume of small requests, and the per-request parsing CPU is what is hurting you, because the C parser and the greenlet-safe connection pool are the point of the project. Do not adopt it if you need HTTP/2, per-request options, or the full feature set of requests, since the README rules all three out. Before you switch an existing codebase, try the requests-compatible interface first, because that path is one import change and the httpx-compatible one drops per-request options.

## FAQ

### How do I install geventhttpclient?

Run `pip install geventhttpclient`. It requires gevent 25.9 or newer, and wheels are provided for common platforms; otherwise the package builds from source and needs a C compiler, because the HTTP parser is a compiled extension built from the vendored llhttp sources.

### Is geventhttpclient a drop-in replacement for requests?

The import surface is, since you can use `import geventhttpclient as requests`, but the feature set is not. The README states it is not a full replacement for requests or httpx, covers the common cases such as cookies, form data, compressed bodies and proxies, has no HTTP/2 support, and does not plumb per-request options through because the engine is configured at session level.

### What is the difference between the concurrency setting and the gevent pool size?

The concurrency argument limits the number of connections in the pool, and greenlets beyond that limit wait for a free connection, while the gevent pool size is how many greenlets run concurrently. Having more greenlets than connections is fine, and the README's example uses a pool of 50 greenlets against 10 connections.

### How do I use httplib2 or urllib with geventhttpclient?

Patch them before importing, using geventhttpclient.httplib.patch(). The README warns that for httplib2 the patch has to happen before the import, otherwise its super() calls fail. The project also provides the missing gevent.httplib patching functionality that was removed in gevent 1.0.

## Sources

- [geventhttpclient/geventhttpclient on GitHub](https://github.com/geventhttpclient/geventhttpclient)
- [Issues](https://github.com/geventhttpclient/geventhttpclient/issues)
- [README](https://github.com/geventhttpclient/geventhttpclient/blob/master/README.md)
- [Releases](https://github.com/geventhttpclient/geventhttpclient/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/geventhttpclient-geventhttpclient
