Files
http-latency-prober/docs/usage/hxprobe.md
Jan Novak f487a4b1bd docs: hxprobe plans, summaries, explanations, usage, and changelog
Plans (docs/plans/):
- 2026-07-01-23-47-py-hxprobe-httpx.md — initial httpx probe design
- 2026-07-02-09-32 through 14-05 — standalone project, toolchain, usage
  doc + Makefile, file input (-f), simplification pass, run-summary footer

Summaries (docs/summaries/): one per completed feature, recording what
was actually built, deviations from the plan, and verification steps

Explanations (docs/explanations/): two deep-dives written during review —
hxprobe concurrency model and worst-exit-code + render-loop analysis

Usage (docs/usage/hxprobe.md): overview with pointer to hxprobe/USAGE.md
for the full runnable reference

Walkthrough (docs/py-latprobe-walkthrough.md): narrative tour of the
latprobe Python package for interview / code-review context

CHANGELOG.md: entries for all hxprobe features (toolchain, usage doc,
file input, simplification, run-summary footer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 13:22:59 +02:00

9.4 KiB

hxprobe/ — httpx-based HTTP latency probe (Go-client parity)

For a full case-by-case runnable reference (every flag, every failure mode, real captured output), see hxprobe/USAGE.md.

What it does

A standalone Python CLI, independent of anything else in this repo (own pyproject.toml, own venv — see Setup). It measures per-phase HTTP latency like python/latprobe, but is built on the httpx library instead of raw sockets, so the client matches Go's http.DefaultClient: HTTP/2 negotiated via ALPN, redirects followed by default, connection pooling, and default TLS verification — while still reporting the same six-phase breakdown (DNS, TCP connect, TLS, TTFB, Transfer, Total).

Python has no equivalent of Go's net/http/httptrace, so the phase timing is recovered by instrumenting httpx's network backend directly (hxprobe/probe.py): a custom NetworkBackend/NetworkStream pair times DNS, TCP connect, and TLS at the socket level, while HTTP framing (HTTP/1.1 or HTTP/2), redirect-following, and keep-alive stay entirely owned by httpx.

Run it from the hxprobe/ directory with uv run python -m hxprobe, or via make hx-run ARGS="…" from the repo root.

Setup

hxprobe is a fully self-contained project — it could be copied out of this repo into its own tomorrow and still work, with its own pyproject.toml, lockfile (uv.lock), and uv-managed venv (hxprobe/.venv), separate from anything under python/. Install once:

make hx-deps

This runs uv sync inside hxprobe/, creating .venv and installing httpx (plus h2 for HTTP/2), pytest, and ruff from uv.lock — pinned, reproducible versions, not whatever the resolver happens to pick at install time. It's a prerequisite of make hx-run/make hx-test/etc., so those targets set it up automatically on first run — make hx-deps is only needed if you want to call uv run python -m hxprobe directly from inside hxprobe/. See hxprobe/README.md for the from-inside-the-directory quick start.

Flags / arguments

Same surface as latprobe, plus two opt-outs for the Go-like defaults:

python -m hxprobe [flags] <url> [url ...]
Flag Default Description
url … (positional) required One or more URLs to probe
-n N, --count N 1 Number of requests per URL
-c N, --concurrency N 0 (auto) Max parallel URLs; 0 = min(len(urls), 8)
--timeout DURATION 10s Per-request timeout; supports ms, s, m, or bare seconds
--fail off Exit non-zero when any HTTP status ≥ 400
--json off Output as JSON array instead of text
-v, --verbose off Show resolved IP, negotiated protocol/redirects, TLS info, certificate, response headers
--no-http2 off (HTTP/2 on) Disable HTTP/2 negotiation, force HTTP/1.1
--no-follow-redirects off (follow on) Report the raw redirect response instead of following it
-h, --help Show help and exit 0

Exit codes: identical to latprobe (0 ok, 1 usage, 2 dns, 3 connect, 4 timeout, 5 tls, 6 http≥400 with --fail); the highest code across all URLs is returned. When more than one URL is probed, a summary footer is appended after the last URL block tallying every URL's outcome (ok / dns / connect / timeout / tls / http) and the resulting exit code — see "Multi-URL summary footer" in hxprobe/USAGE.md for a real example.

Examples

Single URL — negotiates HTTP/2 by default

make hx-run ARGS="https://example.com"
https://example.com  (200)
  DNS lookup     :      2.81 ms
  TCP connect    :      8.28 ms
  TLS handshake  :     16.13 ms
  Server (TTFB)  :      1.04 ms
  Transfer       :      0.62 ms
  ─────────────────────────────
  Total          :     42.53 ms

Verbose — shows the negotiated protocol

make hx-run ARGS="-v https://example.com"
https://example.com  (200)
  DNS lookup     :      2.93 ms
  TCP connect    :      8.58 ms
  TLS handshake  :     17.71 ms
  Server (TTFB)  :      4.35 ms
  Transfer       :      0.84 ms
  ─────────────────────────────
  Total          :     44.06 ms
  IP             :  104.20.23.154
  Protocol       :  HTTP/2
  TLS            :  TLSv1.3  TLS_AES_256_GCM_SHA384  256 bit
  Cert           :  CN=example.com  valid until 2026-08-29  SSL Corporation

Redirects followed by default

make hx-run ARGS="-v http://github.com"
http://github.com  (200)
  DNS lookup     :     14.01 ms
  TCP connect    :     19.42 ms
  TLS handshake  :     22.72 ms
  Server (TTFB)  :      0.01 ms
  Transfer       :     65.66 ms
  ─────────────────────────────
  Total          :    196.72 ms
  IP             :  140.82.121.4
  Protocol       :  HTTP/2  (1 redirect)
  TLS            :  TLSv1.3  TLS_AES_128_GCM_SHA256  128 bit
  Cert           :  CN=github.com  valid until 2026-08-02  Sectigo Limited

Pass --no-follow-redirects to get the raw redirect response instead:

make hx-run ARGS="--no-follow-redirects http://github.com"
http://github.com  (301)
  DNS lookup     :      2.85 ms
  TCP connect    :     24.16 ms
  Server (TTFB)  :     24.50 ms
  Transfer       :      0.59 ms
  ─────────────────────────────
  Total          :     52.58 ms

Forcing HTTP/1.1

make hx-run ARGS="-v --no-http2 https://example.com"
https://example.com  (200)
  DNS lookup     :      2.31 ms
  TCP connect    :      8.75 ms
  TLS handshake  :     13.61 ms
  Server (TTFB)  :     13.51 ms
  Transfer       :      0.44 ms
  ─────────────────────────────
  Total          :     39.07 ms
  IP             :  104.20.23.154
  Protocol       :  HTTP/1.1
  TLS            :  TLSv1.3  TLS_AES_256_GCM_SHA384  256 bit
  Cert           :  CN=example.com  valid until 2026-08-29  SSL Corporation

Sampling, JSON, --fail, DNS failure

Same shape as latprobe's own examples — see py-latprobe.md for -n, --json, multi-URL, --fail, and timeout output. hxprobe's JSON verbose object adds two keys:

"verbose": {
  "ip": "104.20.23.154",
  "http_version": "HTTP/2",
  "redirect_count": 1,
  "tls_version": "TLSv1.3",
  ...
}

http_version/redirect_count are omitted from the text view when http_version is unknown (only happens on failures before headers arrived).

HTTP/2, redirects, and a TTFB accuracy note

HTTP/2 and redirects are the headline reasons this project exists alongside the raw-socket latprobe — see py-latprobe.md for what that implementation does instead (always HTTP/1.1, never follows redirects).

A TTFB finding worth knowing, if you've compared numbers against another raw-socket HTTP client: during development, this implementation's TTFB came in consistently ~40-50ms lower than a comparable raw-socket implementation that didn't set TCP_NODELAY on its connect socket. That gap wasn't noise — a socket that never sets TCP_NODELAY is subject to Nagle's algorithm interacting with the server's delayed-ACK timer, a well-known ~40ms artifact. This implementation sets TCP_NODELAY on every connection (matching both httpcore's own default backend and Go's net.Dialer), avoiding that penalty. Forcing TCP_NODELAY onto the other socket experimentally collapsed its TTFB to match this one's — confirming the cause. If you're comparing hxprobe's numbers against some other HTTP/1.1 client that doesn't set TCP_NODELAY, expect this implementation's TTFB to read lower, and correctly so.

Redirect semantics for dns/connect/tls vs ttfb/transfer: when redirects are followed, dns/connect/tls (and the verbose IP/TLS/cert fields) reflect the first connection only — "cost of reaching the origin server." ttfb/transfer reflect the last hop, because each write/read call overwrites them — which mirrors how Go's own httptrace.ClientTrace hooks behave for a followed redirect (they aren't guarded either, so the last hop wins there too).

Limitations

  • Always GET, no custom headers/body/auth — matches Go's http.DefaultClient.
  • Options.timeout applies uniformly to connect/read/write/pool phases (a single value); it is not split into separate per-phase budgets.
  • No UNIX socket support.
  • HTTP/2 requires TLS (https://) in practice — cleartext h2c is not attempted for http:// URLs (matches nearly every real HTTP/2 deployment).

Makefile targets

make hx-deps                                   # one-time: uv sync (venv + lockfile install)
make hx-run ARGS="-v -n 3 https://example.com" # run it
make hx-test                                   # hermetic unit tests (pytest -m "not integration")
make hx-test-integration                       # live tests: real HTTP/2 negotiation, redirects
make hx-lint                                   # ruff check
make hx-fmt                                    # ruff format
make hx-check                                  # lint + hermetic tests (the pre-commit-style gate)

hx-check (lint + hermetic tests) also runs as part of the repo-root make check; hx-test runs as part of make test.