Timing breakdown of an HTTP(S) request: DNS lookup, TCP connect, TLS handshake, time to first byte, content transfer.
pip install -e .Python 3.11+, no runtime dependencies.
reqphase https://example.comHTTP/1.1 200 OK
559 bytes
DNS Lookup 4.1ms ###
TCP Connect 1.5ms #
TLS Handshake 5.6ms ####
Server Processing 46.9ms ################################
Content Transfer 0.2ms #
Total 72.4ms
Bars are scaled relative to the slowest phase in that particular request, not to a fixed axis, so a single request's chart is easy to read at a glance.
A bare host without a scheme is treated as http://:
reqphase example.com:8080-k/--insecure skips TLS certificate verification (self-signed certs,
internal CAs). --timeout sets the socket timeout in seconds (default 10).
--no-color disables ANSI output for piping into a file or another tool.
Machine-readable output for scripts:
reqphase --json https://example.com | jq .timings_ms{
"dns_lookup": 4.1,
"tcp_connect": 1.5,
"tls_handshake": 5.6,
"server_processing": 46.9,
"content_transfer": 0.2,
"total": 72.4
}-L/--location follows redirects instead of stopping at the first 3xx.
Every hop is measured, but only the final response gets the full phase
breakdown; the hops in between are summarised above it:
301 http://github.com -> https://github.com/
HTTP/1.1 200 OK
...
With --json, the hops show up under a redirects key instead.
--max-redirects (default 5) caps how many hops it will chase before giving
up with an error.
| code | meaning |
|---|---|
| 0 | got a response (any status code) |
| 2 | usage error (bad arguments) |
| 3 | request failed - DNS, connect, TLS, timeout |
reqphase doesn't turn a 4xx/5xx response into a failing exit code by itself; it measured a real response, which is a success from its point of view. Check the status in the output if you need that.
There's no curl subprocess and no requests underneath. reqphase resolves
the host itself (socket.getaddrinfo), opens the TCP connection itself, does
the TLS handshake itself if the URL is https, and then speaks just enough
HTTP/1.1 by hand to send the request line and read back a status line,
headers and body. Every one of those steps gets its own timestamp, which is
what makes the phase breakdown possible: server processing ends the moment
the first byte of the response arrives, content transfer is everything after
that until the body is fully read (respecting Content-Length or chunked
encoding, falling back to reading until the connection closes).
I wanted the old httpstat output on a machine where I didn't want to pull in
curl or fight its -w template syntax, and doing the socket-level timing by
hand felt like a more interesting weekend than reaching for requests.
The idea and the shape of the output (phase-by-phase timing with a small
bar chart) come from httpstat, which
wraps curl's -w timing variables and has been without a release since 2020.
reqphase is a clean-room reimplementation: no code was carried over, and the
approach is different underneath - stdlib sockets instead of shelling out to
curl. Attribution and thanks to the original for making the idea obvious in
the first place.
One request, one process, HTTP/1.1 only. No HTTP/2, no connection reuse
across requests, no request bodies (GET-shaped tools only, so no -d/-X
flags). If you need any of that, curl's -w output or a proper HTTP client
is the better tool; reqphase stays small on purpose.
pip install -e ".[dev]"
pytest
ruff check .MIT. See LICENSE.