HTTP/1.1, HTTP/2, HTTP/3 and TCP probes¶
ICMP tells you the path is up. It does not tell you whether a web server answers, or whether HTTP/3 is buying anything over HTTP/2 on your link. Four probes answer that, all of them shipped inside the SmokePing container — no fork of SmokePing, no fork of the LinuxServer image.
| probe | class | what one sample is | tool |
|---|---|---|---|
CurlHTTP1 |
Curl |
one HEAD https://<host>/ over HTTP/1.1, DNS excluded |
static curl |
CurlHTTP2 |
Curl |
the same over HTTP/2 (TLS + ALPN h2) |
static curl |
CurlHTTP3 |
Curl |
the same over HTTP/3 (QUIC, --http3-only) |
static curl |
TCPPing |
TCPPing |
SYN → SYN/ACK to port 443, nothing above the transport | tcpping / tcptraceroute |
The HTTP probes take 3 samples per 5-minute cycle (pings = 3,
step = 300), the TCP probe 5, five targets in parallel. Nine HTTP targets
plus three TCP targets cost about 40 requests per 5 minutes. Each HTTP sample is a HEAD request: a TLS
handshake and the response headers, about 12 KB over HTTP/1.1 and HTTP/2
and 18 KB over HTTP/3 (Measurement budget). Keep pings low all the
same; 20 requests per cycle is unfriendly to the server.
What the Curl sample measures¶
This is the HTTP protocol layer: does the server answer over this
version, and how fast. SmokePing's Curl probe records
time_total − time_namelookup, and the request is a HEAD (-I), so
that is TCP or QUIC connect, TLS handshake, request, and the response
headers. DNS is excluded, so a slow resolver does not show up as a slow
site (the DNS probes are for that). Redirects are not
followed; a 301 is timed as a 301.
The status code is not checked. Some servers answer HEAD / with a 405
(www.netflix.com) or a 503 (www.amazon.com's bot protection); that is
still the server answering over that version, after a full handshake, and
it is timed like a 200. A failure is curl failing: no connection, a TLS
error, a timeout, or the wrong version (below).
Before this was a HEAD, it was a GET of the whole home page, every
sample. That cost 1.3 MB per sample for www.cloudflare.com, about 5.7 GB
a day for that one seed target over three versions, and the time recorded
was mostly the download: 1.85 s for www.netflix.com against 0.56 s to the
first byte. An upgrade switches installed probes to HEAD once, in the
database and in the config dir's probes.yaml (a probe whose extraargs
you edited is left alone), so the HTTP series steps down
at the upgrade. A full page load is a different measurement, of the
application rather than the protocol, and does not belong on a 5-minute
step.
The three HTTP probes share one binary, /usr/local/bin/curl-h3, a static
stunnel/static-curl build with
ngtcp2/nghttp3 pinned by version and sha256 in
shared/modules/smokeping/Dockerfile.
Alpine's curl (and the official curlimages/curl) are built without a
QUIC backend, so HTTP/3 needs its own binary — and once it exists, HTTP/1.1
and HTTP/2 use it too on purpose: one TLS stack for all three, so a
difference between versions is the protocol, not the build.
The version is enforced, not just requested¶
--http2 asks for HTTP/2 and quietly accepts HTTP/1.1 if the server does
not offer h2. Charted under "HTTP/2", that sample would be a lie. The
probes close that hole with two Curl probe variables and no custom code:
extrare = /;/
extraargs = --http2;-s;-I;-o;/dev/null;-w;Time: %{time_total} DNS time: %{time_namelookup} Redirect time: %{time_redirect} HTTPv=%{http_version}\n
expect = HTTPv=2
require_zero_status = yes
extraargs come after the probe's own -w, and curl keeps the last -w,
so the output line the probe parses is ours; it repeats the fields the
probe's regex needs and appends the negotiated version. expect then
requires HTTPv=2 somewhere in the output, or the sample is dropped. A
downgrade, a failed QUIC handshake, a timeout: all count as loss, which is
what the "Failed fetches" panel shows. extrare = /;/ splits extraargs
on ; instead of spaces because the -w format has spaces in it.
Verified in the container with smokeping --debug: every sample line
reads HTTPv=1.1, HTTPv=2 or HTTPv=3 for its probe.
How the four probes are configured¶
In probes.yaml (per edition under config-manager/config/, defaults in
shared/modules/config-manager/templates/probes.yaml):
probes:
CurlHTTP2:
module: Curl # the SmokePing class; name is ours
binary: /usr/local/bin/curl-h3
step: 300
pings: 3
forks: 5
timeout: 10
urlformat: https://%host%/
require_zero_status: 'yes'
extrare: '/;/'
extraargs: '--http2;-s;-I;-o;/dev/null;-w;Time: ... HTTPv=%{http_version}\n'
expect: HTTPv=2
TCPPing:
binary: /usr/bin/tcpping
step: 300
pings: 5
forks: 5
port: 443
module is the one key that is ours rather than SmokePing's. Three probes
of the same class become SmokePing sub-probes: the generator emits
+ Curl once, then ++ CurlHTTP1, ++ CurlHTTP2, ++ CurlHTTP3, each
with its full configuration (SmokePing treats the + section as a
template of defaults when sub-sections exist). Targets reference the
sub-probe name: probe = CurlHTTP3.
In database mode the same information lives in the probes table:
module and a JSON options column hold what does not fit the native
binary/step/pings/forks columns. Both columns are added to an existing
table on startup (DatabaseManager.create_tables()), and an
already-migrated deployment picks up the new probes, the new http and
tcp categories, and those categories' YAML targets on its next start —
categories the database already knows are left alone, since the database
is the source of truth from then on.
Targets¶
Three sites that serve all of HTTP/1.1, 2 and 3 ship as examples, plus the same three over TCP:
http:
- {name: Google_h1, host: www.google.com, title: Google HTTP/1.1, probe: CurlHTTP1, category: http}
- {name: Google_h2, host: www.google.com, title: Google HTTP/2, probe: CurlHTTP2, category: http}
- {name: Google_h3, host: www.google.com, title: Google HTTP/3, probe: CurlHTTP3, category: http}
tcp:
- {name: Google_tcp443, host: www.google.com, title: Google TCP 443, probe: TCPPing, category: tcp}
Keep the _h1 / _h2 / _h3 suffix. The exporters read the HTTP
version back from it (probe_type = http1 / http2 / http3; a target
in the HTTP section without a suffix is tagged plain http), and the
Grafana dashboard groups the three versions of one site by stripping it.
The web-admin add form appends the suffix for you when you pick HTTPS
fetch and a version; it also offers TCP connect (port 443).
host is a hostname, not a URL: the probe asks for https://<host>/. To
probe a path or a different port, change urlformat on the probe (it
applies to every target of that probe) or add another sub-probe.
Where the data lands¶
| section (RRD dir) | measurement | probe_type tag |
|---|---|---|
HTTP/ |
http_latency |
http1, http2, http3 (or http) |
TCP/ |
tcp_latency |
tcpping |
Same shape as dns_latency: ping1..ping5 and median in seconds,
loss as a 0–1 ratio (InfluxDB) or packet_loss in percent (ClickHouse).
Both exporters classify by the top-level RRD directory, which is the
section name config_generator.CATEGORY_PRESENTATION emits — renaming one
means renaming the other.
The dashboard HTTP by Version – Side-by-Side (InfluxDB and ClickHouse variants) shows, per site, the median response time of the three versions on one panel, the failed-fetch share next to it, and the TCP handshake floor for all targets underneath. The ClickHouse variant was checked on 2026-09-19 against a ClickHouse 24.1 fed by the exporter from the reference Pi's RRDs: the site variable resolves and all three panels return rows through Grafana's query API. It has not been looked at in a browser.
Reading the numbers¶
- TCP handshake ≈ one RTT. Everything above it is protocol cost. On a 10 ms path, an HTTP/1.1 fetch at 100 ms is 1 RTT of TCP + 2 of TLS 1.2 (1 of TLS 1.3) + 1 of request/response + server think time.
- HTTP/3 is not faster on a clean link. QUIC's wins are loss recovery and connection migration; on a wired, lossless path it often lands within a few ms of HTTP/2, sometimes behind (UDP in userspace). Where it should pull ahead is exactly when the loss panel is non-zero — the point of charting the three together.
- Failed fetches on one version only is the interesting signal: HTTP/3 failing while 1.1 and 2 succeed usually means UDP/443 is being dropped or rate-limited somewhere on the path (a middlebox, a hotel network, a carrier). All three failing is the site or the path.
- First cycle after a restart may show HTTP/3 loss on sites that
advertise
h3only viaAlt-Svc—--http3-onlydoes not consult it, so every target here must accept a cold QUIC connection. The three shipped ones do.
Adding a probe that is not shipped¶
Three tiers, none of which is a fork:
- Shipped class + shipped binary (
Curl,TCPPing,DNS,SSH,TraceroutePing, ...): configuration only, as above. - Shipped class, missing binary (
opensslfor a raw TLS probe,mtr): oneapk addin the smokeping Dockerfile. - A class SmokePing does not have: a
.pmfile in this repo,COPY'd into/usr/share/smokeping/Smokeping/probes/. SmokePing loads anySmokeping::probes::Fooby name. A ~40-line subclass ofCurlthat swaps the-wformat totime_appconnect − time_connectwould chart the TLS handshake alone, without the OpenSSL CLI.
The EchoPing* probes the template used to list are gone: echoping is
unmaintained and not packaged, so they could never have run.