Architecture

Server metrics architecture

TurboPanel collects host metrics from connected daemons via authenticated POST /api/daemon/v1/metrics โ€” approximately one sample per minute per server. Metrics is disposable, statistical, and may be sampled; it is not a billing ledger or audit trail.

Canonical source

Agent maintenance detail lives in

instance/AGENTS.md

and

daemon/AGENTS.md

(Host metrics). Operations: Metrics deployment.

Overview

One versioned wire contract (METRICS_SCHEMA_VERSION = 1) is mirrored in the daemon and instance repos (not build-coupled). Storage is dual-runtime:

RuntimeStoreBackend
Cloudflare WorkersAnalytics EngineSERVER_METRICS binding โ†’ turbopanel_server_metrics dataset
Self-hosted DenoClickHouseClickHouseServerMetricsStore on 127.0.0.1:8123

Store selection: resolveServerMetricsStore (always on โ€” Workers โ†’ Analytics Engine, Deno โ†’ ClickHouse).

HTTP request body (protocol v1):

{
  "type": "metrics",
  "version": 1,
  "at": "2026-07-12T12:00:00.000Z",
  "intervalSeconds": 60,
  "sequence": 42,
  "metrics": { "cpuUsagePercent": 12.5, "load1": 0.8, "..." : "..." },
  "dimensions": { "schemaVersion": 1, "daemonVersion": "โ€ฆ", "operatingSystem": "โ€ฆ", "architecture": "โ€ฆ", "kernelRelease": "โ€ฆ" }
}

Scheduling: 60 s interval, โ‰ค5 s deterministic per-serverId phase jitter, monotonic process-local sequence (resets on daemon restart). Independent of cell ping/heartbeat liveness โ€” see daemon AGENTS.md. Host metrics are ingested only via authenticated POST /api/daemon/v1/metrics; WebSocket { type: "metrics" } frames are no longer accepted.

20-metric contract

Order is fixed โ€” positional AE doubles and ClickHouse columns depend on it (HOST_METRIC_KEYS).

#MetricUnit / semantics
1cpuUsagePercentPercent 0โ€“100 (total CPU)
2cpuUserPercentPercent 0โ€“100
3cpuSystemPercentPercent 0โ€“100
4cpuIowaitPercentPercent 0โ€“100
5load1Load average (1 min)
6load5Load average (5 min)
7load15Load average (15 min)
8memoryUsedPercentPercent 0โ€“100
9memoryUsedBytesBytes
10memoryAvailableBytesBytes
11swapUsedPercentPercent 0โ€“100
12diskUsedPercentPercent 0โ€“100 (root filesystem /)
13diskReadBytesPerSecondBytes per second (aggregate disks)
14diskWriteBytesPerSecondBytes per second
15diskReadOpsPerSecondOperations per second
16diskWriteOpsPerSecondOperations per second
17networkReceiveBytesPerSecondBytes per second (excludes lo)
18networkTransmitBytesPerSecondBytes per second
19processCountCount
20uptimeSecondsSeconds

Missing values are null on the wire โ€” never coerced to 0. First-sample rate metrics (disk/network per-second) are null until a second delta exists.

Analytics Engine mapping (Workers)

SlotContent
indexes[0] / index1Authenticated serverId UUID only โ€” never org, account, hostname, or metric name
double1 โ€ฆ double20Host rows (blob1 = "host"): metrics 1โ€“20 in table order above
blob1Event type discriminator โ€” "host" (metrics sample) or "status" (connection-status transition; see below)
blob2Schema version ("1")
blob3Host rows only: daemonVersion
blob4Host rows only: operating system
blob5Host rows only: architecture
blob6Host rows only: kernel release
blob7 โ€ฆ blob20Reserved empty strings on host rows; blob7 carries the transition reason on status rows

AE doubles have no null. Missing metrics store AE_MISSING_METRIC_SENTINEL = -1e308 โ€” never 0. All host metrics are โ‰ฅ 0, so the sentinel cannot collide. On the AE SQL read path, compare against -pow(10, 308) (scientific-notation literals and NULLIF are not documented for AE SQL).

Connection-status event stream (blob1 = "status")

Every genuine online/offline transition on a server row (not a heartbeat or identity-only touch) also writes one row into this same dataset/table, discriminated from host samples by blob1:

SlotContent
blob1"status"
blob2Schema version (same slot as host rows)
blob7Transition reason โ€” "connect" | "disconnect" | "sweep_stale" | "self_heal" (closed set)
double11 (connected) or 0 (disconnected)
all other doubles/blobsMissing-metric sentinel / empty string โ€” a status row carries no host metrics

AE stamps its own ingestion timestamp for status rows; ClickHouse stores the event's own timestamp directly (the same host-vs-ClickHouse asymmetry host samples already have). Both backends expose this via GET /api/client/v1/servers/:id/metrics/connection, which resolves prior state before the range plus in-range transitions into shared uptime/downtime math so AE and ClickHouse report identical numbers for the same query.

Volume impact is negligible against the write-volume formulas below โ€” status rows are written only on a state flip (at most a handful per server per day in normal operation), not once per sampling interval like host rows, so they do not materially change the write-volume math in this doc.

History-only โ€” never authoritative for liveness

This event stream (and the /metrics/connection endpoint built on it) exists purely for historical uptime/downtime reporting. It is asynchronous, best-effort, and sampled/disposable like all server metrics. The Postgres server.connected / server.status_changed_at columns (see Daemon cell architecture) are the sole source of truth for whether a server is online right now โ€” never gate a current-liveness decision on Analytics Engine or ClickHouse status history.

Query aggregation (SQL API): filter blob1 = 'host' and supported blob2; rows under result.data (Cloudflare v4 envelope). Weighted average per bucket:

SUM(if(doubleN = -pow(10, 308), 0.0, _sample_interval * doubleN))
/ SUM(if(doubleN = -pow(10, 308), 0.0, _sample_interval * 1.0))

Metrics is sampled โ€” always weight by _sample_interval; do not assume one row per minute.

Hosted retention: Cloudflare Analytics Engine retains data for 3 months. UI/API max range is 90 days on Workers; do not imply long-term persistence beyond AE retention on hosted deployments.

ClickHouse schema (self-hosted)

Database turbopanel_metrics; DDL is idempotent (ensureSchema once per process): CREATE TABLE IF NOT EXISTS plus ALTER โ€ฆ MODIFY SETTING (compact-part thresholds) and ALTER โ€ฆ MODIFY TTL so retention changes apply to existing tables.

ClickHouse stores the same positional column layout as Analytics Engine โ€” timestamp, index1 (serverId), double1..double20 (in HOST_METRIC_KEYS order), and blob1..blob20 (identity dimensions + reserved slots) โ€” in a single physical table named turbopanel_server_metrics (same as the AE dataset). Missing metrics use the same AE_MISSING_METRIC_SENTINEL (-1e308) as Analytics Engine โ€” never null or coerced 0. Query aggregates exclude the sentinel with the same filtering semantics as AE; resolution is chosen at query time (no rollup tables or materialized views).

TableRoleDefault TTL
turbopanel_server_metricsMergeTree raw samples (ORDER BY (index1, timestamp))TURBOPANEL_SERVER_METRICS_RETENTION_DAYS (90)

Late arrivals / duplicates: all inserts accepted (no ReplacingMergeTree / FINAL).

Query API & caching

EndpointPurpose
GET /api/client/v1/servers/:id/metrics/seriesTime-series buckets for selected metrics
GET /api/client/v1/servers/:id/metrics/summaryAggregated summary for a range
GET /api/client/v1/servers/:id/metrics/connectionUptime/downtime history from the blob1 = "status" event stream (history-only โ€” see above)

Session + server read grant required. One combined backend query per (server, range) โ€” not per metric or chart.

Resolution ladder: range โ‰ค 6 h โ†’ 60 s; โ‰ค 24 h โ†’ 300 s; โ‰ค 30 d โ†’ 3600 s; else 86400 s (ClickHouse maps 60 โ†’ 300). Max 1500 points; range โ‰ค 90 days.

Chart cache: key includes authorized serverId, bucket-rounded range, sorted metrics, resolution, backend, schema version. TTL: live 45 s / historical 300 s.

Cost model

Metrics ingestion runs on the normal Worker isolate (or Deno process) and no longer incurs Durable Object GB-sec โ€” see Daemon Cell for DO billing. The Analytics Engine price constants, limits, and formulas below remain the single source of truth for AE cost planning.

Verified: 12 July 2026 (Cloudflare docs updated 2026-04-23); currently not billed

Cloudflare states Analytics Engine is currently not billed. The constants below are for capacity planning only โ€” not product pricing. TurboPanel product tiers remain on turbopanel.io/pricing.

Cloudflare Analytics Engine limits & prices

ItemWorkers PaidWorkers Free
Writes10M/mo included, then $0.25/M100k/day
Reads (SQL API)1M/mo included, then $1.00/M10k/day
Retention3 months3 months
Per writeDataPointโ‰ค 250 data points/invocation; โ‰ค 20 doubles, 20 blobs, 1 index; blobs โ‰ค 16 KB; index โ‰ค 96 Bsame

All 20 metrics = one writeDataPoint per sample (one AE data point per minute per server).

Write volume formulas

ConstantValue
Samples per server per day86400 / intervalSeconds = 1,440 @ 60 s
Samples per server per 30-day month43,200
Monthly AE writes (fleet)connectedServers ร— (2,592,000 / intervalSeconds)
Included write allowance10,000,000/mo
Break-even (connected servers @ 1 min)10,000,000 / 43,200 โ‰ˆ **231** servers before write overage
Write overage costmax(0, monthlyWrites โˆ’ 10,000,000) / 1,000,000 ร— $0.25

Read volume (dashboards)

Reads scale with dashboard views รท cache TTL (live 45 s / historical 300 s), not chart count or fleet size โ€” one combined query per server/range/resolution, cache-collapsed.

ItemValue
Included SQL reads1,000,000/mo
Read overage$1.00 per million

Worked examples (60 s interval, all servers connected 24ร—7)

Connected serversMonthly writesWrite overageEst. write cost/mo
1004,320,0000$0.00
1,00043,200,00033,200,000$8.30
5,000216,000,000206,000,000$51.50

Cloudflare currently does not bill Analytics Engine โ€” treat overage dollars as planning figures until billing activates.

Edit on GitHub

Last updated on

On this page