Quick Reference · async HTTP client + server · Python / asyncio

aiohttp cheat sheet

Async all the way down. Open one ClientSession, async with session.get(url) as resp: to borrow a pooled connection, then await resp.json() — the body is a coroutine, and resp is only alive inside the async with. The very same library is also a web server (aiohttp.web). No sync API: everything runs on the asyncio event loop.

setup / async shape the request the response session · connectors · auth async: ws · streaming aiohttp.web server gotcha most common

Introspected from aiohttp 3.14 & cross-checked across: docs.aiohttp.org (official) · Real Python · BetterStack · Superfast Python · Proxieshub / ScrapeOps · yarl / multidict docs

The async round trip — and aiohttp’s two faces on one event loop
BAND A · ONE REQUEST, AWAITED Open ONE Session async with aiohttp.ClientSession() as session: reuse it for every call — holds the connection pool, cookies, headers, auth Send a request async with session.get(   url, params=, json=,   headers=, auth=,   timeout=) as resp: borrows a live connection from the pool for this block Server handles the verb One Response (resp) ready NOW (sync attrs): resp.status · resp.ok · resp.headers but the BODY is a coroutine — you must await it: await resp.json() await resp.text() .read() valid ONLY inside the async with — then the connection returns to the pool resp.raise_for_status() · resp.content is a streaming StreamReader send BAND B · ONE LIBRARY, TWO DIRECTIONS — CLIENT & SERVER SHARE THE LOOP CLIENT — outbound ClientSession TCPConnector · pool (limit=100) session.get / post / ws_connect requests you make to other servers asyncio event loop one thread, many awaited tasks SERVER — inbound aiohttp.web.Application router · add_routes([web.get…]) async handler → web.json_response requests other clients make to you
quickstart.py — the client (left) & the server (right)
import aiohttp, asyncio

async def main():
    # ONE session, reused for every call
    async with aiohttp.ClientSession() as session:
        async with session.get(
                "https://api.example.com/data",
                params={"q": "cats"},
                timeout=aiohttp.ClientTimeout(total=10),
        ) as resp:
            resp.raise_for_status()
            data = await resp.json()   # await!
            print(resp.status, data)

asyncio.run(main())
# same lib = a web server
from aiohttp import web

async def hello(request):
    name = request.query.get(
        "name", "world")
    return web.json_response(
        {"hello": name})

app = web.Application()
app.add_routes([
    web.get("/", hello)])

web.run_app(app)  # :8080
01Setup & Importinstall once
02The Async Shapeno sync API
03HTTP Verbson the session
04Query Parametersthe ?key=value
05Sending a Bodyjson vs data
06Custom Headersper call or session
07The Response Objectsync attrs
08Reading the Body — await itcoroutines!
09Status & raise_for_statuserrors don't raise
10The ClientSessionone per app
11Connectors & Poolingtune the pool
12Timeoutsdefault = 5 min
13Errors & ExceptionsClientError root
14Authenticationbasic & bearer
15Streaming & DownloadsStreamReader
16File Uploads & FormDatamultipart
17WebSocketsbuilt in
18Cookiesjar per session
19aiohttp.web Serverthe other half

Four pictures that make aiohttp click

The temporal shape of one request, the exception tree, why one session matters, and the four ways a request can time out.

1 · The life of one request

Headers arrive first and are ready synchronously; the body is a separate awaited step — and resp only lives inside the async with.

time async with session.get() as resp connection borrowed from pool headers arrive resp.status / .ok / .headers ready NOW (no await) await the body await resp.json() / .text() / .read() block exits conn returned ⚠ read the body AFTER the block → ClientConnectionError

2 · The exception hierarchy

All descend from ClientError — except the total-timeout, which is a plain asyncio.TimeoutError.

ClientError ConnectionError ClientResponseError Payload InvalidURL Connector Error DNS / SSL ServerConn Disconnected / ServerTimeout ContentType Error TooMany Redirects asyncio.TimeoutError the TOTAL timeout — NOT a ClientError catch ClientError and asyncio.TimeoutError

3 · One session, not one per request

A fresh session per call rebuilds the pool every time — slow, and it leaks connectors.

✗ session per request ClientSession #1 ClientSession #2 ClientSession #3 new conn new conn new conn 3 handshakes · no reuse · “Unclosed client session” ✓ one shared session ClientSession one connector one pool req 1 · req 2 · req 3 reuse the kept-alive connection handshake

4 · The four ClientTimeout fields

Each guards a different span of one request. Only total is set by default — to 300s.

start connected 1st byte done sock_connect connect pool+TCP sock_read max gap between reads total — the whole operation default = ClientTimeout(total=300, sock_connect=30) override per session or per request; total=None disables

Worth memorizing

async-onlyno sync API — async def + await, driven by asyncio.run()
await the bodyresp.text()/.json()/.read() are coroutines — await them
status has no ()resp.status is an int attr; only body readers take ()
read inside async withafter the block the conn is released → body read fails
one session/appnot one per request — that kills pooling & leaks connectors
always closeasync with ClientSession() or await s.close(); else “Unclosed”
default timeouttotal=300s (5 min) + sock_connect=30s — usually set your own
json() is strictraises ContentTypeError unless JSON; content_type=None skips
total timeoutraises asyncio.TimeoutError, NOT a ClientError — catch both
resp.url = yarl.URLnot a str — wrap in str(resp.url) when needed
4xx/5xx don't raiseuse raise_for_status() or raise_for_status=True
it's a server tooaiohttp.web: Application, routes, web.run_app
WebSockets built insession.ws_connect(); one reader task, many writers
HTTP/1.1 onlyno HTTP/2 client — need it? use httpx
TCPConnector(limit=100)total cap; limit_per_host=0 = unlimited by default
FormDatamultipart & file uploads; json=/data=/params= mirror requests