Back to Blog
Python

httpx Client Sessions: Cookies and Connection Pooling

python httpx client sessions cookies and connection pooling: Learn how httpx.Client manages cookies and connection pooling, why sessions matter for HTTP integrations,...

httpxconnection poolingcookiesHTTP clientPython
Illustration of multiple HTTP requests flowing through a single pooled connection with a cookie jar beside it, representing httpx client sessions.

python httpx client sessions cookies and connection pooling requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you work with python httpx client sessions, cookies and connection pooling are the two behaviors that most often separate a well-structured integration from a script that reconnects on every request.

When you make a request with httpx.get() or httpx.post(), httpx creates a temporary client, performs the request, and discards the client. Each call opens a fresh connection to the server, and any cookies returned by the server are lost when the call finishes. For a one-off health check that is fine. For an integration that makes many requests to the same host, it wastes resources and breaks stateful flows.

A httpx.Client instance solves both problems. It owns a connection pool that reuses open connections to the same host, and it holds a cookie jar that stores cookies sent by the server and attaches them to subsequent requests. Understanding how these two mechanisms work is the difference between an integration that behaves correctly under load and one that fails intermittently or leaks connections.

The rest of this article covers the behavior of httpx.Client sessions, how cookies are stored and sent, how connection pooling works, and where the defaults are worth changing.

Creating a Client and Making Your First Request

The simplest way to use a client is with a context manager, which guarantees the client is closed when the block exits:

import httpx with httpx.Client() as client: response = client.get("https://api.example.com/status") print(response.status_code)

Closing the client releases the connections in its pool. If you create a client without a context manager, you must call client.close() explicitly; otherwise the underlying connections may remain open until the garbage collector runs.

A client can also be configured at construction time. The most common settings are base_url, headers, cookies, timeout, and limits. A base_url lets you write relative paths in every request:

with httpx.Client(base_url="https://api.example.com") as client: response = client.get("/users/42")

The client applies base_url to any request whose URL is relative. This keeps the host configuration in one place instead of repeating it in every call.

How Cookies Persist Across Requests in a Client

The cookie jar is the reason a client behaves like a session. When a server responds with a Set-Cookie header, httpx stores that cookie in the client's jar. On the next request to a matching domain and path, httpx adds the cookie to the Cookie header automatically.

Consider a login flow where the server sets an authentication token:

with httpx.Client() as client: login = client.post("https://api.example.com/login", json={"user": "alice"}) # Server responds with Set-Cookie: session_id=abc123 profile = client.get("https://api.example.com/profile") # The session_id cookie is sent automatically

You do not need to read the cookie from the login response and copy it into the next request. The client handles that for you, as long as both requests use the same client instance.

You can inspect the stored cookies through client.cookies:

with httpx.Client() as client: client.get("https://api.example.com/login") print(client.cookies.get("session_id"))

The default jar is an http.cookiejar.CookieJar, which applies the standard cookie rules for domain, path, and expiration. Cookies that expire are not sent on later requests, and cookies scoped to a different domain are not attached to requests for that domain.

Controlling the Cookie Jar Manually

Sometimes you want to seed the jar before the first request, for example when a token was obtained out-of-band. Pass a dictionary to the cookies parameter:

with httpx.Client(cookies={"session_id": "abc123"}) as client: response = client.get("https://api.example.com/dashboard")

The client merges these cookies into its jar and continues to store any cookies the server sets. You can also modify the jar after construction:

client = httpx.Client() client.cookies.set("session_id", "abc123")

For more control, pass a custom http.cookiejar.CookieJar instance. This is useful when you need to persist cookies across client lifetimes, for example by saving the jar to disk between runs:

import http.cookiejar jar = http.cookiejar.CookieJar() with httpx.Client(cookies=jar) as client: client.get("https://api.example.com/login")

Because the jar is a regular Python object, you can serialize it with pickle or use http.cookiejar's file-based loaders if your application needs to resume a session after a restart.

How Connection Pooling Works in httpx

Each httpx.Client owns an HTTPConnectionPool. When you make a request, the client checks whether a usable connection to the target host already exists. If it does, the request reuses that connection instead of opening a new TCP socket and performing a new TLS handshake. This reuse is what makes repeated requests to the same host noticeably cheaper.

The pool is keyed by the connection parameters: the scheme, host, port, and the TLS configuration. Requests to different hosts use different connections, so pooling helps most when your workload targets one host or a small set of hosts.

The pool also keeps connections alive after a response completes, up to a configurable limit. A keep-alive connection can serve a later request without the overhead of reconnecting. If the server closes an idle connection, the client transparently opens a new one on the next request.

The practical effect is that a loop of requests to the same endpoint reuses a single connection:

with httpx.Client() as client: for _ in range(100): response = client.get("https://api.example.com/health")

Each iteration reuses the pooled connection rather than creating a new one. This reduces latency and avoids exhausting file descriptors under sustained load.

Tuning Connection Limits and Timeouts

The default pool limits are reasonable for most workloads, but they are worth adjusting when your application makes many concurrent requests or talks to many different hosts.

The limits parameter accepts an httpx.Limits instance:

limits = httpx.Limits(max_connections=100, max_keepalive_connections=20) with httpx.Client(limits=limits) as client: ...

max_connections caps the total number of connections the pool may hold. max_keepalive_connections caps how many idle connections are kept open for reuse. When the keep-alive limit is reached, the oldest idle connections are closed.

The default max_connections is 100 and the default max_keepalive_connections is 20. If your application opens many short-lived clients or talks to many distinct hosts, lowering max_keepalive_connections can reduce the number of idle sockets held open. If you run many concurrent requests against one host, raising max_connections prevents requests from waiting for a free connection.

Timeouts are configured separately through the timeout parameter. A single timeout value applies to all phases of a request:

with httpx.Client(timeout=10.0) as client: response = client.get("https://api.example.com/slow-endpoint")

You can also pass an httpx.Timeout instance to set different values for connect, read, write, and pool timeouts. The pool timeout is the time a request waits for a connection to become available when the pool is exhausted. Setting it too low can cause spurious errors under concurrency; setting it too high can make requests hang when the pool is saturated.

Thread Safety, Context Managers, and Cleanup

A httpx.Client is not thread-safe. Sharing one client across multiple threads without synchronization can produce corrupted state, because the cookie jar and the connection pool are mutated during requests. If your application is multi-threaded, you have two realistic options: give each thread its own client, or guard access to a single client with a lock.

For concurrent workloads, httpx.AsyncClient is the more natural fit. It provides the same cookie and connection-pooling behavior through an async interface:

import asyncio import httpx async def fetch_all(): async with httpx.AsyncClient() as client: responses = await asyncio.gather( client.get("https://api.example.com/a"), client.get("https://api.example.com/b"), ) return responses

The async client pools connections the same way, and its cookie jar behaves identically. The difference is that requests can run concurrently without blocking the event loop.

Cleanup matters in both cases. Failing to close a client leaves idle connections open until the process exits or the garbage collector reclaims the client. In a long-running service, this can accumulate sockets. The context manager form handles cleanup for you, so prefer it unless you have a reason to manage the client's lifetime manually.

When a Client Is Not the Right Choice

A httpx.Client is not always the right tool. If you make a single request to a server that does not set cookies and that you will not contact again, the overhead of constructing a client is unnecessary. The module-level functions like httpx.get() are a reasonable shortcut in that case, though they still create a temporary client internally.

The cookie jar can also cause subtle bugs if you reuse a client across unrelated logical sessions. Cookies from one user's login can leak into another user's requests if you share a single client across requests that should be isolated. In that scenario, create a fresh client per session, or clear the jar between sessions with client.cookies.clear().

Connection pooling is also less beneficial when your requests spread across many unrelated hosts. The pool still works, but each new host requires a new connection, so the reuse benefit is limited to repeated requests to the same host. In that case, the main value of the client is still the cookie jar and the centralized configuration, not the connection reuse.

python httpx client sessions cookies and connection pooling: | RYUSLOG DEV