---
title: "The built-in dashboard"
description: "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

```bash
# 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:

```python
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 to `1m`, `5m`, `15m` or all of
  it; the ring holds 360 samples, so `15m` may be everything the ring has.
* **Refresh can be turned off.** The **refresh** control has an `off` option
  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
  reads `live`, `stale`, `lost` or `connecting`. 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 reads `0` with `nothing 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`/`End` to 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 `None` unless a dashboard is
  running in this process; an ordinary install pays one `is None` comparison 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 `BackstopState`s via
  weakrefs and aggregated in the snapshot. A session is visible exactly as long
  as its owner holds it.
* **Demo mode is process-isolated.** `--demo` runs 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 computes `sample_age_s` locally. 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 a
  `live` dashboard rejects a demo workload outright (`ValueError`).
* **Honest nulls.** With no live sessions the budget KPI is `null`, not a
  misleading `0`. 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 (run `backstop verify` for a full replay).

## Security model

* **Loopback by default.** Without a token the server refuses to bind any
  non-loopback address (`ValueError` from `serve()` and the CLI).
* **Bearer tokens.** `--token` (or `token=`) requires
  `Authorization: Bearer <token>` on every request, compared with
  `hmac.compare_digest`.
* **Restrictive CSP, no external origins.** `default-src 'none'` with
  self-only styles/scripts; the page cannot phone home. Responses also carry
  `nosniff`, `no-referrer`, `frame-ancestors 'none'`, and `X-Frame-Options`.
* **No write surface.** Only `GET`/`HEAD` are served; the dashboard never
  mutates a budget, a circuit, or any other state.
* **XSS-safe rendering.** The page builds DOM nodes with `textContent` only —
  no `innerHTML` — 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.
