httpx logging: disable INFO lines, enable debug (tested)

The INFO:httpx:HTTP Request: ... line appears whenever root logging is at INFO. Hide it with logging.getLogger("httpx").setLevel(logging.WARNING); for connection-level detail set "httpcore" to DEBUG.

httpx logs through the standard logging module: the httpx logger writes one INFO line per request, and the httpcore loggers write connection-level DEBUG lines. Neither has handlers or a level of its own, so what you see depends entirely on your root logging config. Measured with httpx 0.28.1 on Python 3.13.5 against a local http.server.

1. Where the "HTTP Request: GET ..." INFO line comes from

import logging, httpx, srv
url = srv.start()
logging.basicConfig(level=logging.INFO)
httpx.get(url + "/users")
print("logger names:", sorted(n for n in logging.root.manager.loggerDict if n.startswith(("httpx", "httpcore"))))

Output:

INFO:httpx:HTTP Request: GET http://127.0.0.1:8964/users "HTTP/1.1 200 OK"
logger names: ['httpcore', 'httpcore.connection', 'httpcore.http11', 'httpcore.proxy', 'httpx']

As soon as anything calls logging.basicConfig(level=logging.INFO) (your app, a framework, or a library such as an SDK built on httpx), every request prints this line. The srv module in these blocks is a small local test server.

2. Disable / suppress httpx logging

import logging, httpx, srv
url = srv.start()
logging.basicConfig(level=logging.INFO)
logging.getLogger("httpx").setLevel(logging.WARNING)
httpx.get(url + "/users")
logging.getLogger("myapp").info("my own INFO line still shows")

Output:

INFO:myapp:my own INFO line still shows

Raise the level of just the httpx logger. Your own INFO logging is unaffected. Do this after basicConfig or anywhere at startup; the order does not matter because the level lives on the logger. To silence connection debug output too, do the same for "httpcore".

3. httpx debug logging: see the connection steps

import logging, httpx, srv
url = srv.start()
logging.basicConfig(level=logging.DEBUG, format="%(name)s %(levelname)s %(message)s")
with httpx.Client() as c:
    c.get(url + "/users")

Output:

httpcore.connection DEBUG connect_tcp.started host='127.0.0.1' port=8964 local_address=None timeout=5.0 socket_options=None
httpcore.connection DEBUG connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x7f82acbc7620>
httpcore.http11 DEBUG send_request_headers.started request=<Request [b'GET']>
httpcore.http11 DEBUG send_request_headers.complete
httpcore.http11 DEBUG send_request_body.started request=<Request [b'GET']>
httpcore.http11 DEBUG send_request_body.complete
httpcore.http11 DEBUG receive_response_headers.started request=<Request [b'GET']>
httpcore.http11 DEBUG receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Server', b'BaseHTTP/0.6 Python/3.13.5'), (b'Date', b'Fri, 23 Oct 2026 14:14:41 GMT'), (b'Content-Length', b'2')])
httpx INFO HTTP Request: GET http://127.0.0.1:8964/users "HTTP/1.1 200 OK"
httpcore.http11 DEBUG receive_response_body.started request=<Request [b'GET']>
httpcore.http11 DEBUG receive_response_body.complete
httpcore.http11 DEBUG response_closed.started
httpcore.http11 DEBUG response_closed.complete
httpcore.connection DEBUG close.started
httpcore.connection DEBUG close.complete

At DEBUG level httpcore.connection shows TCP connect and close, and httpcore.http11 shows each stage of the request with the raw response headers. This is the quickest way to see whether a hang is in connect, sending, or waiting for response headers. Over HTTPS there are also TLS steps. Turn it on for one logger only with logging.getLogger("httpcore").setLevel(logging.DEBUG) while root stays at INFO.

4. Default: nothing is logged

import logging, httpx, srv
url = srv.start()
logging.basicConfig(level=logging.WARNING, format="%(name)s %(levelname)s %(message)s")
for name in ("httpx", "httpcore"):
    print(name, logging.getLogger(name).getEffectiveLevel(), logging.getLogger(name).handlers)
httpx.get(url + "/users")
print("nothing logged at WARNING")

Output:

httpx 30 []
httpcore 30 []
nothing logged at WARNING

Both loggers have no handlers and inherit WARNING (30) from root, so a plain script prints nothing. If you see request lines you did not ask for, something set the root level to INFO or lower.

5. Custom request/response logging with event hooks

import logging, time, httpx, srv
url = srv.start()
logging.basicConfig(level=logging.INFO, format="%(name)s %(message)s")
logging.getLogger("httpx").setLevel(logging.WARNING)
log = logging.getLogger("api")

def log_request(request):
    request.extensions["t0"] = time.perf_counter()
    log.info("-> %s %s", request.method, request.url)

def log_response(response):
    ms = (time.perf_counter() - response.request.extensions["t0"]) * 1000
    log.info("<- %s %s (%d bytes, %s)", response.status_code, response.request.url,
             len(response.read()), "fast" if ms < 1000 else "slow")

with httpx.Client(event_hooks={"request": [log_request], "response": [log_response]}) as c:
    c.get(url + "/users", params={"page": 2})

Output:

api -> GET http://127.0.0.1:8964/users?page=2
api <- 200 http://127.0.0.1:8964/users?page=2 (2 bytes, fast)

Event hooks run for every request on the client. The response hook runs before the body is read: response.content there raises ResponseNotRead, so call response.read() first if you need the body or its size.

6. AsyncClient event hooks must be async

import asyncio, logging, httpx, srv
url = srv.start()

def sync_hook(response):
    print("sync hook got", response.status_code)

async def async_hook(response):
    print("async hook got", response.status_code)

async def main():
    async with httpx.AsyncClient(event_hooks={"response": [async_hook]}) as c:
        await c.get(url + "/users")
    async with httpx.AsyncClient(event_hooks={"response": [sync_hook]}) as c:
        try:
            await c.get(url + "/users")
        except TypeError as e:
            print("TypeError:", e)

asyncio.run(main())

Output:

async hook got 200
sync hook got 200
TypeError: object NoneType can't be used in 'await' expression

With AsyncClient, a plain def hook still runs, then httpx tries to await its None return value and raises TypeError. Use async def hooks (and await response.aread() for the body).

Related: httpx raise_for_status in a response hook, httpx timeout, httpx MockTransport for tests, httpx vs requests differences.

Frequently asked questions

How do I stop httpx logging every request?

logging.getLogger("httpx").setLevel(logging.WARNING). The INFO line only appears when the root logger is at INFO or lower.

How do I enable httpx debug logging?

Set logging.getLogger("httpcore").setLevel(logging.DEBUG) (or basicConfig(level=logging.DEBUG)); httpcore.connection and httpcore.http11 log each connection step.

Why do my AsyncClient event hooks raise TypeError?

AsyncClient awaits every hook; a sync def hook returns None, which cannot be awaited. Make the hook async def.

Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1 on Python 3.13.5 against a local http.server. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx is free.