A Heliograph smoke graph with three vantage lines (local, comcast, fios) overlaid

Heliograph: rebuilding SmokePing in Go

Projects Sep 10, 2026

Source: github.com/seitzbg/heliograph

SmokePing has been around for twenty years and still does one thing better than anything else: its "smoke" latency graph. The percentile bands show jitter. The loss-coloured median line shows drops. Together they tell you how a path behaves over time instead of just whether it is up. I did not want the rest of it though. It is Perl. It stores data in RRD files. Changing the config means editing text on the box and reloading. So I rewrote it. Heliograph is that graph in Go, backed by TimescaleDB.

It keeps the smoke graphs and the pluggable probe model. It drops the Perl and the RRD files. It adds two things SmokePing does not have: multi-vantage federation and a browser-editable config. The daemon is still called smoked.

Why Go

SmokePing forks a process per probe. Go's goroutines make parallel probing with per-target timeouts cheap. Heliograph runs one bounded worker pool. Every target gets its own schedule and timeout. Both binaries build with CGO_ENABLED=0 so the container and FreeBSD builds stay simple.

The graph

A smoke graph is only trustworthy if it reflects the real spread of round-trip times rather than an average. So Heliograph keeps the full sample array for every round. Each round is one row in a TimescaleDB hypertable. Loss is stored as SQL NULL rather than zero. The percentile bands get computed at query time. Hourly and daily continuous aggregates keep long time ranges fast. The dashboard redraws the graph on an HTML canvas. Nested percentile bands darken toward the median over an eight-bucket loss-coloured line, and it follows the site theme.

Probes

There are eight built-in probes behind a self-registering plugin registry: ICMP, an fping wrapper, TCP connect, DNS, HTTP time-to-first-byte, SSH banner timing, IRTT, and NTP clock offset. The native ping is pure Go. It uses an unprivileged datagram socket with a raw-socket fallback, so there is no fping dependency and no setcap step in the common case.

# Measure a demo target set and print a table
go run ./cmd/smoked -rounds 2 -pings 10

# Serve the live dashboard + JSON API, persisting to TimescaleDB
go run ./cmd/smoked -serve -addr :8087 \
  -dsn 'postgres://user:pass@host:5432/smoke?sslmode=disable'

Federation

Remote smoke-agent collectors report to the hub and overlay a median line per vantage. You can watch one target from several networks at once and see where the paths split. There are no API keys. smoked bootstraps its own certificate authority and issues each vantage a client certificate that acts as its identity. That runs over mutual TLS on a listener that is never proxied. Revoking a vantage means removing its name. Its next request is then rejected even though the certificate is still valid.

Config in the database

The target tree lives in the database. You edit it from a Config tab in the browser with drag-to-reorder. Every target keeps a stable server-assigned id. Renaming or moving a node preserves its graph history and its alert state, because reconciliation happens by identity rather than by name.

An MCP server

smoked mcp exposes thirteen tools so an assistant can diagnose a path or edit the config. The write path is narrow on purpose. The staging and review tools work on an in-process buffer that the daemon's own parser validates. Only config_apply writes anything, and it sits behind -admin-pass.

Where it is

Heliograph is at v2.2.0 under the MIT licence. There are prebuilt GHCR images, native FreeBSD binaries with rc.d scripts, and a SmokePing importer (smoked import smokeping, with --history to backfill old RRD data). It is at github.com/seitzbg/heliograph. If you have a tired SmokePing box the migration is meant to be uneventful.

Tags