Skip to content

Python HTTP clients compared: requests, httpx, aiohttp and urllib3

requests has no default timeout, httpx gave up after 5 s, urllib3's request() after 12. And httpx.get() in a loop ran 18 times slower than a Client.

HProxy TeamOctober 12, 2026Updated October 12, 202610 min read

Python HTTP clients compared: requests, httpx, aiohttp and urllib3

Python has two HTTP clients built in, urllib.request and http.client. Four more are popular on PyPI: urllib3, requests, httpx and aiohttp. They all send the same requests in the end, but their defaults differ more than their features.

We ran all six against one test server on 12 October 2026, with Python 3.12.3, and left every setting at its default. httpx gave up on a silent server after 5 seconds and urllib3's request() after 12. The other four were still waiting when we stopped them. One httpx habit also made it 18 times slower than it needed to be.

The six at a glance

ClientInstallSync or asyncFollowed our redirectRaised on our 404On a server that never answers
urllib.requestbuilt insyncyesyesstill waiting at 15 s
http.clientbuilt insyncnonostill waiting at 15 s
urllib31 package, 0.9 MBsyncyesnogave up at 12.1 s with request()
requests5 packages, 3.1 MBsyncyesnostill waiting at 15 s
httpx7 packages, 3.9 MBbothnonogave up at 5.1 s
aiohttp10 packages, 13.6 MBasyncyesnostill waiting at 15 s

One request in each client

Each of these asked example.com for its home page. That domain exists for examples like these: IANA keeps it "for documentation purposes". Each one sets a timeout, for reasons the timeout section below shows.

urllib.request comes with Python:

import urllib.request

with urllib.request.urlopen("https://example.com/", timeout=10) as r:
    print(r.status, len(r.read()))

http.client is the layer underneath it. You open the connection and send the path yourself. Python's own documentation says "It is normally not used directly", and adds: "The Requests package is recommended for a higher-level HTTP client interface".

import http.client

conn = http.client.HTTPSConnection("example.com", timeout=10)
conn.request("GET", "/")
r = conn.getresponse()
print(r.status, len(r.read()))
conn.close()

urllib3 keeps pools of open connections. requests is built on it.

import urllib3

pool = urllib3.PoolManager(timeout=10)
r = pool.request("GET", "https://example.com/")
print(r.status, len(r.data))

requests is the one most tutorials use. A Session keeps connections open between calls:

import requests

with requests.Session() as session:
    r = session.get("https://example.com/", timeout=10)
    print(r.status_code, len(r.content))

httpx looks like requests, and adds an async mode and HTTP/2. A Client plays the part of a Session:

import httpx

with httpx.Client(timeout=10) as client:
    r = client.get("https://example.com/")
    print(r.status_code, len(r.content))

aiohttp is async only, so every call runs inside an event loop:

import asyncio

import aiohttp


async def main():
    timeout = aiohttp.ClientTimeout(total=10)
    async with aiohttp.ClientSession(timeout=timeout) as session:
        async with session.get("https://example.com/") as r:
            print(r.status, len(await r.read()))


asyncio.run(main())

All six printed the same line:

200 577

What each one sends

Our test server listed the headers each client sent, with every setting left alone:

ClientUser-AgentAccept-Encoding
urllib.requestPython-urllib/3.12identity
http.clientnoneidentity
urllib3python-urllib3/2.8.0identity
requestspython-requests/2.34.2gzip, deflate
httpxpython-httpx/0.28.1gzip, deflate
aiohttpPython/3.12 aiohttp/3.14.5gzip, deflate

The three lower-level clients ask for no compression, so pages arrive at full size. http.client sends no User-Agent at all, and urllib.request adds Connection: close, so it never reuses a connection. Our guide to request headers covers what to send instead.

The defaults that bite

Redirects

We asked each client for a page that answers with a 302 to another page. Four followed it and returned the second page with a 200. http.client and httpx returned the 302 itself. httpx does this on purpose: "By default, HTTPX will not follow redirects for all HTTP methods, although this can be explicitly enabled". Pass follow_redirects=True when you want the target page.

Errors

Only urllib.request raised an exception for a 404. Python's documentation describes the chain: "Eventually, HTTPDefaultErrorHandler will raise an HTTPError if no other handler handles the error". The other five returned the 404 as a normal answer, so check the status, or call raise_for_status() in requests and httpx.

With nothing listening on the port, all six failed at once. Each raised its own exception: URLError, ConnectionRefusedError, urllib3's MaxRetryError, requests' ConnectionError, httpx's ConnectError and aiohttp's ClientConnectorError.

Timeouts

The test that matters most: a server that takes the request and never answers. We set no timeout and stopped each client after 15 seconds:

Terminal output: six Python HTTP clients against a server that never answers, a closed port, the cost of creating a client, 1,000 requests in a row and 200 requests at once
Our own capture, 12 October 2026: clients_test.py on our server, steps 4 to 8
  • httpx gave up after 5.1 seconds with a ReadTimeout. Its documentation: "The default behavior is to raise a TimeoutException after 5 seconds of network inactivity".
  • urllib3 gave up after 12.1 seconds, but only through the quick urllib3.request() function. That function waits 3 seconds per try (its signature says timeout = 3) and tries again three times by default. A PoolManager you create yourself has no timeout, and it was still waiting at 15 seconds.
  • requests was still waiting. Its documentation: "By default, requests do not time out unless a timeout value is set explicitly".
  • aiohttp was still waiting too. Its limit is 5 minutes: "By default aiohttp uses a total 300 seconds (5min) timeout".
  • urllib.request and http.client were still waiting. Without a timeout they use the socket default, and Python's documentation says "A value of None indicates that new socket objects have no timeout".

So set a timeout in every client, as the examples above do. A scraper with no timeout can stop for good on one bad server.

Speed: one request after another

We sent 1,000 requests in a row to a small page on the same machine, three times per setup, and kept the middle run:

Setup1,000 requestsProcessor time
http.client, one connection1.00 s0.53 s
aiohttp, ClientSession1.03 s0.49 s
urllib3, PoolManager1.12 s0.59 s
urllib.request, urlopen() each time1.84 s0.81 s
httpx, Client1.95 s1.30 s
requests, Session2.35 s1.57 s
requests.get() each time3.39 s2.23 s
httpx.get() each time35.82 s32.20 s

httpx.get() in a loop was 18 times slower than the same requests through one httpx.Client. The cost sits in creating the client. Creating an httpx.Client took 23.7 ms, and building an SSL context from the certifi certificate file took 23.9 ms. A requests Session took 0.011 ms. The top-level httpx.get() creates a new Client on every call: its source runs with Client( for each request. httpx's documentation says it plainly: "If you do anything more than experimentation, one-off scripts, or prototypes, then you should use a Client instance".

requests pays less for the same habit: requests.get() in a loop took 3.39 seconds against 2.35 with a Session. Keep one Session or Client and reuse it.

These requests went to a server on the same machine, so they show each library's own work. On a real network, each request also waits for the round trip. The next test adds that wait.

Speed: many requests at once

Here every request waited 100 ms for its answer, the way a slow site or a far-away server does. We sent 200 of them:

200 requests that take 100 ms each
requests, in a row
23.54 s
requests, 20 threads
1.73 s
httpx, async
0.96 s
aiohttp
0.32 s
Source: HProxy test on our server, 12 October 2026: Python 3.12.3, requests 2.34.2, httpx 0.28.1, aiohttp 3.14.5; a test server on the same machine; middle of 3 runs (1 run in a row) · hproxy.comProxy.

One after another, 200 waits of 100 ms add up: 23.54 seconds. Twenty threads cut that to 1.73 seconds. The two async clients went further, because one process can keep many requests waiting at the same time.

Both async clients open at most 100 connections by default. aiohttp's documentation lists the limit as "(default: 100)", and httpx's as "(Default 100)". So 200 requests need at least two rounds of 100 ms, 0.2 seconds in all. aiohttp came close, with 0.32 seconds.

httpx's AsyncClient took 0.96 seconds and used 0.81 seconds of processor time, where aiohttp used 0.08. So httpx spent its time on its own work, not on waiting for the server. Many requests at once is aiohttp's strength, and the numbers show it.

Both async examples follow the same pattern. With aiohttp, the requests run side by side with asyncio.gather():

import asyncio

import aiohttp

URLS = [
    "https://hproxy.com/blog/web-scraping-with-python",
    "https://hproxy.com/blog/python-requests-retry",
    "https://hproxy.com/blog/python-requests-headers",
]


async def fetch(session, url):
    async with session.get(url) as r:
        await r.read()
        return r.status, url


async def main():
    timeout = aiohttp.ClientTimeout(total=30)
    async with aiohttp.ClientSession(timeout=timeout) as session:
        for status, url in await asyncio.gather(*(fetch(session, url) for url in URLS)):
            print(status, url)


asyncio.run(main())
200 https://hproxy.com/blog/web-scraping-with-python
200 https://hproxy.com/blog/python-requests-retry
200 https://hproxy.com/blog/python-requests-headers

Keep the number of requests to one site modest, whatever the client can do. Our guide to fixing 429 errors explains what happens when a site decides you are too fast.

HTTP/2

We asked our own site, which speaks HTTP/2, for one small file with each client. requests and aiohttp used HTTP/1.1, and so did httpx by default. Only httpx with http2=True used HTTP/2. Its documentation explains that "HTTP/2 support is not enabled by default", and it needs the extra package: pip install "httpx[http2]".

import httpx

with httpx.Client(http2=True, timeout=10) as client:
    r = client.get("https://hproxy.com/robots.txt")
    print(r.status_code, r.http_version)
200 HTTP/2

What you install

We installed each library into a fresh environment and counted what came with it. Then we measured the processor time its import added to a bare Python start, as the middle of seven runs:

LibraryPackagesSizeImport
urllib.request, http.client0, built in059 ms and 44 ms
urllib310.9 MB137 ms
requests53.1 MB173 ms
httpx73.9 MB122 ms
httpx with HTTP/2104.9 MB112 ms
aiohttp1013.6 MB325 ms

aiohttp is the heaviest to install and to import. For a long-running scraper that hardly matters. For a short command-line tool that starts thousands of times, it can.

Which one to use

  • A quick script, nothing to install: urllib.request. Set a timeout, and catch HTTPError, because it raises one for error answers such as a 404.
  • Most scripts and scrapers: requests with a Session, or httpx with a Client. httpx gives you a 5 second default timeout, HTTP/2 and an async mode in one library. Never call httpx.get() in a loop.
  • Hundreds of pages at once: aiohttp was the fastest in our test, and the lightest on the processor. httpx's AsyncClient is the choice if you want one library for both styles.
  • Building your own tool or library: urllib3, or http.client for full control.

Whichever you pick, our requests, httpx and aiohttp proxy guide shows how each of the three takes a proxy. Our retry guide covers what to do when a request fails.

What this page could not check

  • The test server ran on the same machine as the clients. Real servers add network delay, TLS and their own quirks.
  • The times are the middle of three runs on one server, and they move between runs. In three full runs, httpx.get() in a loop took 35.15 to 37.82 seconds. Compare the ratios, not the milliseconds.
  • HTTP/2 was tested against one site, our own, with three clients. urllib3's own HTTP/2 support was not tested.
  • Brotli and zstandard compression were not installed, so no client asked for them.
  • We will rerun these tests by 12 January 2027.

Sources

Frequently asked questions

What is the best Python HTTP client?

For most scripts, requests with a Session, or httpx with a Client. httpx adds a 5 second default timeout, HTTP/2 and an async mode. For hundreds of requests at once, aiohttp was the fastest in our test. With nothing to install, urllib.request works if you set a timeout and catch HTTPError.

Is httpx faster than requests?

With a Client, yes in our test: 1,000 local requests took 1.95 seconds against 2.35 for a requests Session. But calling httpx.get() in a loop took 35.82 seconds, because each call builds a new client, and building one took 23.7 ms.

Is aiohttp faster than httpx?

In our test, yes. 200 requests to a page that took 100 ms each finished in 0.32 seconds with aiohttp and 0.96 seconds with httpx's AsyncClient. httpx used ten times more processor time for the same work.

What is the difference between http.client and requests?

http.client is the low-level module in Python's standard library: you open the connection, send the path and read the answer yourself, and it does not follow redirects. requests sits on top of urllib3 and adds sessions, redirects, cookies and JSON. Python's own documentation recommends requests for a higher-level interface.

Does requests have a default timeout?

No. Its documentation says requests do not time out unless a timeout value is set explicitly. In our test, requests was still waiting on a silent server after 15 seconds. Pass timeout= on every call.

Get proxies that are alive right now

That list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed