The built-in dashboard
A dependency-free operations dashboard: budgets, concurrency, breaker state, sessions and enforcement events, with no Prometheus.
Backstop ships a dependency-free operations dashboard: backstop dashboard.
It answers “what is Backstop doing to my traffic right now?” — budgets, AIMD
concurrency, circuit state, per-session isolation, and enforcement events —
without asking you to stand up Prometheus, Alertmanager, or any exporter first.
Quick start
# Look at it with synthetic traffic (no API keys, no network calls)
backstop dashboard --demo
# Watch a real process: run the dashboard alongside your workload
backstop dashboard # http://127.0.0.1:8787 (loopback only)
backstop dashboard --port 8788 --cost-model gpt-4o
backstop dashboard --audit audit.jsonl # enable the enforcement event stream
# Exposing beyond loopback requires a bearer token
backstop dashboard --host 0.0.0.0 --token "$(openssl rand -hex 32)"
Or serve it at the root of a dedicated FastAPI app:
from fastapi import FastAPI
from fastapi.middleware.wsgi import WSGIMiddleware
from backstop import Backstop
app = FastAPI()
app.mount("/", WSGIMiddleware(Backstop.dashboard_app()))
Subpath mounts such as /dashboard are not currently supported: assets and
snapshot requests use root-relative URLs.
The page polls /api/snapshot once per sample interval (2s live, 1s demo;
--refresh to override). Responses carry an ETag, so an unchanged snapshot
revalidates as 304 Not Modified instead of being re-downloaded. The snapshot
distinguishes render time from data age: sampled_at is the absolute time the
last sample was taken, sample_age_s is how old it is (both null before the
first sample), and sample_interval_s gives the cadence — use
sample_age_s against sample_interval_s to show or warn about staleness.
What it shows
| Panel | Meaning |
|---|---|
| Budget | live limit, spent, remaining, % used, burn rate, projected exhaustion |
| Spend | tokens and a USD lower bound (input-token rate of --cost-model) |
| Prevention | stopped calls, with budget, rate-limit, and circuit block counts |
| Traffic | requests/s, total requests, provider calls, and retries |
| Latency | p95 of observed transport calls, with p50 and p99; provider calls included |
| Concurrency | AIMD active/capacity, queue depth |
| Outcome mix | request counts and proportional bars by outcome |
| Circuit | breaker state, trip count, and the rate of requests the open breaker turned away |
| Isolation | live sessions, per-session rows (id, budget, spend, concurrency) |
| Tenants | per-tenant ledger usage and remaining |
| Events | tail of the audit log (allowlisted fields only) |
| Signals | the runaway-spend detectors: what fired, how far past its threshold, and in which unit |
The methodology footer states exactly what each number is derived from, so a screenshot cannot be mistaken for more than it is.
Reading the dashboard
- The x-axis is wall-clock. Each chart header states the window it shows
(
14:32:05 → 14:36:05 · 4m), so a spike lines up with a log line. The window control slices the in-memory ring to1m,5m,15mor all of it; the ring holds 360 samples, so15mmay be everything the ring has. - Refresh can be turned off. The refresh control has an
offoption that pauses polling; the sample age keeps counting so the data’s age stays visible. An auto-updating surface must be pausable (WCAG 2.2.2). - Status is text first. Every tile carries a status token (
OK,WATCH,BREACH,IDLE,NO DATA) next to its colour, and the connection state readslive,stale,lostorconnecting. Colour is redundant, never load-bearing — which is what keeps amber and red apart for a colour-blind reader. - “No data” is not “zero”. A tile with no samples reads
NO DATA; a tile with samples and nothing blocked reads0withnothing blocked yet. A sparkline with no points draws a dashed baseline, not an empty box. - Charts are readable without a mouse. Every chart has a text summary for
assistive technology and a Show as table toggle that renders the same
numbers as a table. Tooltips are dismissible with
Esc. - Tables take the keyboard. Focus a table and use
↑/↓to move between rows,←/→to scroll,Home/Endto jump. - Tiles drill down. Activating a tile title filters the tables below to the rows that explain that number; the filter chip clears it.
- Deltas are against the previous window. Each tile that has a series shows the change over the preceding window of the same length.
- Enforcement events are marked on the traffic chart, so a guardrail action lines up with the traffic it acted on. A burst of decisions is one marker, not a picket fence.
- The signals panel states the detector’s mode. The runaway-spend detectors report in shadow mode by default — they never block — and the panel says so, along with whether detection is switched on at all.
Every derived figure states its own derivation: the chart window and the unit of each axis are printed in the header or the tooltip, so a screenshot can be argued with.
Where the data comes from
The dashboard renders state the process already holds. Metrics.call() — the
single choke point every instrumented code path already goes through — feeds a
telemetry sink (backstop.telemetry) before the optional Prometheus path, so
the built-in dashboard works in a bare pip install backstop-ai with nothing
installed beyond the stdlib.
Deliberate constraints:
- No storage, no retention, no query language. A fixed-size ring of samples in memory (360 samples ≈ 12 minutes at the 2s default), bounded session rows, event rows, and a 64 KiB audit tail. Nothing is written to disk, nothing survives the process. For retention, fleet-wide queries, and alerting, export Prometheus/OTel and point them at your own monitoring stack — that path is unchanged, and the dashboard reports whether those sinks are active.
- No hot-path cost when unused. The sink is
Noneunless a dashboard is running in this process; an ordinary install pays oneis Nonecomparison per instrumented event. - Live sessions are the source of truth for gauges. Process-global
Prometheus gauges collapse across sessions (last writer wins), so budget and
concurrency figures are read from the registered
BackstopStates via weakrefs and aggregated in the snapshot. A session is visible exactly as long as its owner holds it. - Demo mode is process-isolated.
--demoruns the mock workload (its sessions, counters, and telemetry sink) in a spawned child process that publishes bounded snapshots over shared memory; the demo dashboard reads them and computessample_age_slocally. The demo’s audit records are held in the child’s memory — the audit log accepts a callable sink — so the demo shows real enforcement decisions without writing anything to disk. Construction registers nothing in the parent: demo sessions never appear in the parent’s session registry, demo capture never reaches the parent’s sink, and demo mode cannot install, uninstall, or otherwise disturb a real sink the application is already using. Stopping the dashboard always leaves application-owned capture running, and alivedashboard rejects a demo workload outright (ValueError). - Honest nulls. With no live sessions the budget KPI is
null, not a misleading0. The cost figure is labelled a lower bound because output and cache-read tokens are not priced. The audit stream notes that a tail cannot re-verify the HMAC chain (runbackstop verifyfor a full replay).
Security model
- Loopback by default. Without a token the server refuses to bind any
non-loopback address (
ValueErrorfromserve()and the CLI). - Bearer tokens.
--token(ortoken=) requiresAuthorization: Bearer <token>on every request, compared withhmac.compare_digest. - Restrictive CSP, no external origins.
default-src 'none'with self-only styles/scripts; the page cannot phone home. Responses also carrynosniff,no-referrer,frame-ancestors 'none', andX-Frame-Options. - No write surface. Only
GET/HEADare served; the dashboard never mutates a budget, a circuit, or any other state. - XSS-safe rendering. The page builds DOM nodes with
textContentonly — noinnerHTML— so values containing HTML cannot escape into markup.
CLI reference
| Flag | Default | Meaning |
|---|---|---|
--port |
8787 |
listen port |
--host |
127.0.0.1 |
bind address; non-loopback requires --token |
--refresh |
2.0s live, 1.0s demo | sample interval in seconds |
--cost-model |
none | model whose input rate prices the USD lower bound |
--audit |
none | audit JSONL path; enables the enforcement event stream |
--demo |
off | run the deterministic mock workload (3 lanes, no keys) |
--runners |
3 |
demo lane count (1–8) |
--theme |
auto |
initial colour theme: auto (follow the OS), dark, or light; the page’s theme button overrides per browser and is remembered |
--token |
none | require this bearer token |
--no-browser |
off | do not open a browser tab |
Non-goals
No history beyond the in-memory ring, no multi-process aggregation, no user accounts, no saved views. Anything you would want to keep belongs in Prometheus/OTel, not here.