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:
| Runtime | Store | Backend |
|---|---|---|
| Cloudflare Workers | Analytics Engine | SERVER_METRICS binding โ turbopanel_server_metrics dataset |
| Self-hosted Deno | ClickHouse | ClickHouseServerMetricsStore 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).
| # | Metric | Unit / semantics |
|---|---|---|
| 1 | cpuUsagePercent | Percent 0โ100 (total CPU) |
| 2 | cpuUserPercent | Percent 0โ100 |
| 3 | cpuSystemPercent | Percent 0โ100 |
| 4 | cpuIowaitPercent | Percent 0โ100 |
| 5 | load1 | Load average (1 min) |
| 6 | load5 | Load average (5 min) |
| 7 | load15 | Load average (15 min) |
| 8 | memoryUsedPercent | Percent 0โ100 |
| 9 | memoryUsedBytes | Bytes |
| 10 | memoryAvailableBytes | Bytes |
| 11 | swapUsedPercent | Percent 0โ100 |
| 12 | diskUsedPercent | Percent 0โ100 (root filesystem /) |
| 13 | diskReadBytesPerSecond | Bytes per second (aggregate disks) |
| 14 | diskWriteBytesPerSecond | Bytes per second |
| 15 | diskReadOpsPerSecond | Operations per second |
| 16 | diskWriteOpsPerSecond | Operations per second |
| 17 | networkReceiveBytesPerSecond | Bytes per second (excludes lo) |
| 18 | networkTransmitBytesPerSecond | Bytes per second |
| 19 | processCount | Count |
| 20 | uptimeSeconds | Seconds |
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)
| Slot | Content |
|---|---|
indexes[0] / index1 | Authenticated serverId UUID only โ never org, account, hostname, or metric name |
double1 โฆ double20 | Host rows (blob1 = "host"): metrics 1โ20 in table order above |
blob1 | Event type discriminator โ "host" (metrics sample) or "status" (connection-status transition; see below) |
blob2 | Schema version ("1") |
blob3 | Host rows only: daemonVersion |
blob4 | Host rows only: operating system |
blob5 | Host rows only: architecture |
blob6 | Host rows only: kernel release |
blob7 โฆ blob20 | Reserved 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:
| Slot | Content |
|---|---|
blob1 | "status" |
blob2 | Schema version (same slot as host rows) |
blob7 | Transition reason โ "connect" | "disconnect" | "sweep_stale" | "self_heal" (closed set) |
double1 | 1 (connected) or 0 (disconnected) |
| all other doubles/blobs | Missing-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).
| Table | Role | Default TTL |
|---|---|---|
turbopanel_server_metrics | MergeTree raw samples (ORDER BY (index1, timestamp)) | TURBOPANEL_SERVER_METRICS_RETENTION_DAYS (90) |
Late arrivals / duplicates: all inserts accepted (no ReplacingMergeTree / FINAL).
Query API & caching
| Endpoint | Purpose |
|---|---|
GET /api/client/v1/servers/:id/metrics/series | Time-series buckets for selected metrics |
GET /api/client/v1/servers/:id/metrics/summary | Aggregated summary for a range |
GET /api/client/v1/servers/:id/metrics/connection | Uptime/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
| Item | Workers Paid | Workers Free |
|---|---|---|
| Writes | 10M/mo included, then $0.25/M | 100k/day |
| Reads (SQL API) | 1M/mo included, then $1.00/M | 10k/day |
| Retention | 3 months | 3 months |
Per writeDataPoint | โค 250 data points/invocation; โค 20 doubles, 20 blobs, 1 index; blobs โค 16 KB; index โค 96 B | same |
All 20 metrics = one writeDataPoint per sample (one AE data point per minute per server).
Write volume formulas
| Constant | Value |
|---|---|
| Samples per server per day | 86400 / intervalSeconds = 1,440 @ 60 s |
| Samples per server per 30-day month | 43,200 |
| Monthly AE writes (fleet) | connectedServers ร (2,592,000 / intervalSeconds) |
| Included write allowance | 10,000,000/mo |
| Break-even (connected servers @ 1 min) | 10,000,000 / 43,200 โ **231** servers before write overage |
| Write overage cost | max(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.
| Item | Value |
|---|---|
| Included SQL reads | 1,000,000/mo |
| Read overage | $1.00 per million |
Worked examples (60 s interval, all servers connected 24ร7)
| Connected servers | Monthly writes | Write overage | Est. write cost/mo |
|---|---|---|---|
| 100 | 4,320,000 | 0 | $0.00 |
| 1,000 | 43,200,000 | 33,200,000 | $8.30 |
| 5,000 | 216,000,000 | 206,000,000 | $51.50 |
Cloudflare currently does not bill Analytics Engine โ treat overage dollars as planning figures until billing activates.
Last updated on
Daemon cell architecture
SQLite-backed Durable Object daemon cell for presence, merged request delivery/correlation, and reduced per-reconnect write cost โ hibernation-safe design and Cloudflare billing model
Control Plane
Install and configure the TurboPanel control plane for self-hosted and TurboPanel High Availability deployment