Overview
Time series is the one storage kind where Go is the home team: Prometheus, VictoriaMetrics,
and InfluxDB are all written in Go, and the instrumentation idioms of the whole
cloud-native world were set by prometheus/client_golang. So a Go service usually meets
time-series storage from the producer side — exposing metrics to be pulled — rather than
through a query driver. The write-side clients (Influx line protocol, Timescale via pgx,
remote-write) matter when you push measurements as data rather than expose them as telemetry.
Key points
- client_golang is the canonical instrumentation library:
Counter,Gauge,Histogram,Summary, their*Veclabeled variants, registered on aRegistryand served bypromhttp.Handler()at/metrics. The pull model inverts the driver relationship — Prometheus scrapes you; your only "storage code" is an HTTP handler. - Custom collectors implement
prometheus.Collector(Describe/Collect) to expose state computed at scrape time — pool sizes, queue depths — instead of maintaining gauges by hand.GaugeFuncis the one-metric shortcut. - Exemplars attach a trace ID to a histogram observation
(
ExemplarObserver.ObserveWithExemplar), linking a latency bucket to an exact trace — needs OpenMetrics negotiation on the scrape. - OpenTelemetry metrics (otel-go) is the converging path: instrument once against the OTel API, choose the exporter — Prometheus pull or OTLP push — at the edge. New greenfield code increasingly starts here rather than on client_golang directly.
- InfluxDB — influxdb-client-go: push model, line protocol
(
measurement,tag=v field=1 timestamp), a non-blockingWriteAPIthat batches in the background (listen onErrors()) and a blocking variant for when you need the error now. - TimescaleDB is Postgres: the entire pgx stack
applies unchanged — batch inserts via
CopyFrom, hypertables andtime_bucketare just SQL. No new client to learn is the selling point. - VictoriaMetrics is Go-native and speaks Prometheus remote-write plus Influx line
protocol on ingest; from application code you rarely target it directly — you point
remote-write or vmagent at it.
VictoriaMetrics/metricsexists as a lighter client_golang alternative. - QuestDB ingests via ILP (Influx line protocol) with an official Go client
(
questdb/go-questdb-client); queries over PG-wire — one line, same shape as Timescale. - Cardinality is the failure mode: every distinct label combination is a stored series. No user IDs, no unbounded values in labels; batch client-side writes; pre-aggregate where the query only ever needs the aggregate.
Details
Producer side vs writer side
| Path | Library | Model | When |
|---|---|---|---|
| Expose metrics | client_golang | pull (scrape) | service telemetry — the default |
| Expose metrics, portable | otel-go SDK | pull or OTLP push | greenfield, multi-backend |
| Push measurements | influxdb-client-go | push, line protocol, batched | business/IoT measurements as data |
| SQL time series | pgx → TimescaleDB | SQL, CopyFrom bulk |
relational + time series in one store |
| Remote-write | Prometheus remote-write → VictoriaMetrics | push, snappy-compressed protobuf | long-term storage behind Prometheus |
The split worth internalizing: telemetry about the service goes out the pull path; measurements as domain data (readings, prices, events you query arbitrarily) belong in a store you write to explicitly — Influx, Timescale, QuestDB.
Batching discipline
- Influx/QuestDB clients batch for you — size and flush interval are tunables; watch the async error channel or you will drop points silently.
- With Timescale, single-row INSERTs are the classic mistake: use
pgx.CopyFromor batched multi-row inserts for ingest-heavy paths.
Examples
var reqDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Request latency.",
Buckets: prometheus.DefBuckets,
},
[]string{"route", "method"}, // bounded labels only — never user IDs
)
func init() { prometheus.MustRegister(reqDuration) }
func handler(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
reqDuration.WithLabelValues("/orders", r.Method).
Observe(time.Since(start).Seconds())
}()
// ...
}
// main: http.Handle("/metrics", promhttp.Handler())
Related
- Storage access from Go — the map — parent overview by storage kind.
- Relational databases from Go — the pgx stack Timescale and QuestDB queries ride on.
- Databases and other storage systems — LSM/columnar internals that make time-series stores fast.
- Time-series stores from Java — the JVM mirror: Micrometer as facade where Go grew up on client_golang.
- OpenTelemetry — the converging facade as a concept: API/SDK/Collector, OTLP, and the metrics data model behind the exporters.