Files
http-latency-prober/docs/usage/py-phases.md
Jan Novak 16fff2f964 docs: add usage-phases.md beside usage-simple.md in python/configs/
- Rename python/configs/usage.md → usage-simple.md so both scripts have a
  parallel runnable reference file in the same directory
- Add python/configs/usage-phases.md: runnable sh commands + real expected
  output for phases.py against every error-path config, including a note on
  the key difference from simple.py (HTTP 4xx exits 0 in phases.py vs 1 in
  simple.py because phases.py completes at the network level)
- Update doc cross-links in docs/usage/py-simple.md and py-phases.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:11:53 +02:00

4.6 KiB

phases.py — Per-Phase HTTP Latency Measurement

What it does

Measures the latency of each phase of an HTTP request by hand-driving a raw socket, timing each step individually with time.perf_counter(). This is the Python answer to Go's net/http/httptrace — there is no equivalent callback API in Python's stdlib, so we instrument at the socket level.

Phases measured:

Phase What is timed
DNS lookup socket.getaddrinfo() — hostname resolution
TCP connect sock.connect() — SYN to connection established
TLS handshake ssl.wrap_socket() — full handshake (HTTPS only)
Server (TTFB) sendall() return → first recv() byte — server processing time
Transfer First byte → EOF — body download time
Total DNS start → body EOF — wall-clock end-to-end

The TLS row is omitted automatically for http:// URLs.

Flags / arguments

python phases.py <url> [url ...]
python phases.py <config_file>
Argument Meaning
<url> [url …] One or more URLs starting with http:// or https://
<config_file> Path to a plain-text URL list (same format as simple.py)

Detection is automatic: if the first argument starts with http:// or https://, all arguments are treated as URLs; otherwise the single argument is treated as a config file path.

Exit codes:

Code Meaning
0 All URLs completed without a network error
1 One or more URLs failed, or a usage/config error

HTTP error statuses (4xx, 5xx) do not set exit code 1 — the request completed successfully at the network level. The status code is visible in the output header.

Example — single URL

python phases.py https://example.com
https://example.com  (200)
  DNS lookup     :     21.74 ms
  TCP connect    :     10.79 ms
  TLS handshake  :     18.48 ms
  Server (TTFB)  :     73.72 ms
  Transfer       :      0.13 ms
  ─────────────────────────────
  Total          :    132.95 ms

Example — HTTP URL (no TLS row)

python phases.py http://example.com
http://example.com  (200)
  DNS lookup     :      3.37 ms
  TCP connect    :     13.51 ms
  Server (TTFB)  :     20.01 ms
  Transfer       :      2.85 ms
  ─────────────────────────────
  Total          :     39.77 ms

Example — DNS failure (partial phases)

python phases.py https://no.such.host.invalid
https://no.such.host.invalid  (FAILED)
  ─────────────────────────────
  Total          :      0.67 ms
  ✗ dns: [Errno 8] nodename nor servname provided, or not known

Example — TLS failure (partial phases preserved)

python phases.py https://expired.badssl.com/
https://expired.badssl.com/  (FAILED)
  DNS lookup     :     28.32 ms
  TCP connect    :    129.01 ms
  TLS handshake  :    305.30 ms
  ─────────────────────────────
  Total          :    473.17 ms
  ✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired …

Note: DNS and TCP phases are populated even though the overall request failed. This is the same behaviour as the Go tool — partial timing is preserved up to the point of failure.

Example — config file

python phases.py configs/all-ok.txt

Multiple URLs are separated by a blank line, matching Go's text output style.

Using the error-path example configs

The python/configs/ files from simple.py work identically with phases.py. See python/configs/usage-phases.md for runnable shell commands and expected output for every config file, including a side-by-side comparison of how phases.py and simple.py differ on HTTP 4xx responses.

Limitations

  • No redirect following. 3xx responses are reported with their raw status code; the redirect target is not probed. Use latprobe/ (the full version) or simple.py (which uses urllib, which follows redirects) if you need the final destination's timing.
  • HTTP 1.1 + Connection: close only. No keep-alive, no HTTP/2, no auth, no custom headers beyond Host and User-Agent.
  • Timeout hard-coded at 10 s. Use latprobe/ for a --timeout flag.
  • No sampling. Each URL is probed once. Use latprobe/ for -n (min/avg/max).
  • Body is fully drained. Transfer time includes reading the entire response body, so it is real wall-clock transfer time.