Prometheus metrics: the /metrics scrape endpoint
GET /metrics is a Prometheus text-exposition scrape surface: point a scraper at it and it answers with the standard # HELP / # TYPE preamble and one sample line per series, the exact shape any Prometheus-compatible tool already parses. Nothing about the wire format needs teaching. This is the Tier 1 surface named on the monitoring overview: the effort is in reaching the endpoint and reading the credential correctly, not in getting your tool to understand what comes back.
The endpoint reads the same run-history and replication state that already drives the backup-failure, backup-stale and replication-degraded alerts described in alert events. It computes nothing new; it renders the same facts in a different shape for a different reader.
Minting the metrics credential
The endpoint is reached by a bearer credential, not a console session, so an unattended scrape running every fifteen or thirty seconds never has to hold a browser cookie or pass a CSRF check. The credential is a third scope on the exact mechanism owner-minted pull credentials documents for vendor diagnostics and the SIEM audit feed: an Owner mints it, the secret is shown once, and only its SHA-384 hash is stored (mintIngestCredential, engine/src/admin/support-ingest.ts).
| Scope | Default lifetime | Maximum | What it serves |
|---|---|---|---|
diagnostics |
72 hours | 7 days | GET /support/diagnostics, the signed support bundle |
audit-feed |
90 days | 365 days | GET /support/audit-feed, the hash-chained audit events |
metrics |
365 days | 400 days | GET /metrics, the Prometheus scrape surface |
The metrics scope carries the longest lifetime of the three on purpose: it is meant to sit in a scrape target’s configuration for the long run, the same way a Prometheus job’s bearer file usually does, rather than a lease you renew every quarter. It still expires on the engine’s own clock and is still revocable at any moment, the same as the other two.
Mint it from the console on the Integrations screen. Open a metrics vendor’s tile (Prometheus is the obvious one) and its panel carries the “Metrics scrape (Prometheus)” credential to mint, with the Metrics endpoint URL shown right above it to point a scraper at, a copy button included. The mint is Owner-only, and once a second Owner exists on the account it is dual-control gated the same way minting a diagnostics or audit-feed credential is: the first attempt queues a pending approval and a second Owner acts before the token is issued. The secret appears once; capture it then, because only its hash is stored. (The diagnostics scope, for a support pull, still lives under Settings; the audit-feed and metrics scopes moved onto their Integrations tiles.)
Or mint it against the API
An Owner can mint the same token directly, for a scripted setup. This is the route the console itself calls, so the dual-control gate applies either way:
POST /admin/support/credentials HTTP/1.1
Host: console.example.com
Content-Type: application/json
{"scope": "metrics"}{
"scope": "metrics",
"clientId": "dpc_AbCdEfGhI",
"secret": "dps_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"bearer": "dpc_AbCdEfGhI.dps_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expiresAt": "2027-07-05T00:00:00.000Z",
"note": "the secret is shown once and stored only as a hash; revoke and re-mint to rotate"
}Revoke with POST /admin/support/credentials/delete.
Reaching the endpoint
The engine serves /metrics itself, and in the shipped single-hostname topology the console worker proxies it through to the engine over the ENGINE service binding, alongside /admin/* and /support/* (isEngineSurface, console/src/worker.ts; see the same-origin proxy). So a scrape aimed at https://console.example.com/metrics reaches the engine on the default topology, with no second hostname to stand up. The metrics tile’s panel shows the exact URL to use, derived from your own origin, so you copy it rather than assemble it by hand.
If Cloudflare Access fronts the console, the scrape is turned away at the edge
The metrics endpoint rides the console hostname, so a Cloudflare Access application in front of that hostname turns an unattended scrape away before the engine ever sees the bearer: the edge answers with a redirect to your team’s login page, whatever credential the scraper carries. The console warns you about this at mint time when it detects the perimeter. The minted credential still gates the scrape; you also have to let the scraper past Access, and which of the three arrangements below fits depends on whether your scraper can send custom request headers.
In every case, create a path-scoped Access application over /metrics first. In the Cloudflare Zero Trust dashboard add a self-hosted application whose path is exactly /metrics on your console hostname (for example console.example.com/metrics). A more specific application takes precedence over the identity policies covering the rest of the hostname, so this scopes the change to the scrape path alone.
-
A Service Auth policy and a service token (recommended). Give that application a Service Auth policy and issue an Access service token. The scraper sends
CF-Access-Client-IdandCF-Access-Client-Secreton every request, alongside the metrics bearer inAuthorization. Prometheus sets these with thehttp_headersoption in the scrape config (CF-Access-Client-Idundervalues,CF-Access-Client-Secretundersecrets), and the OpenTelemetry Collector’sprometheusreceiver and the Grafana Agent set request headers too. Keep the service token in its own two headers; do not fold it intoAuthorization, which already carries the metrics bearer.scrape_configs: - job_name: downpipes metrics_path: /metrics scheme: https authorization: type: Bearer credentials: <the minted metrics bearer> http_headers: CF-Access-Client-Id: values: [<access-service-token-client-id>] CF-Access-Client-Secret: secrets: [<access-service-token-client-secret>] static_configs: - targets: ["console.example.com"] -
A narrow Bypass, for a scraper that cannot set request headers. A tool that cannot add request headers (an older Prometheus from before the
http_headersoption, or a constrained scraper) cannot present theCF-Access-*headers a Service Auth policy needs. Give the same path-scoped application a Bypass policy instead. Access then stops gating/metrics, and the minted metrics bearer is the sole gate on that path, which is its designed posture: a bearer secret, constant-time verification, and a plain401on failure. Cloudflare’s own guidance is to prefer Service Auth over Bypass and to scope a Bypass as narrowly as possible, so reserve this for scrapers that genuinely cannot present the token. -
The engine’s own hostname. The split topology puts the engine’s
/metricson a separate hostname with its own Access policy (the split topology), so the scrape never crosses the console’s perimeter. That is the exception, not the normal case.
The split topology, where the engine takes its own custom domain (the split topology), works too: point the scraper at the engine’s own hostname, which the engine answers directly. It is no longer required for scraping, though; the default single hostname reaches the endpoint on its own.
Pointing a scraper at it
With the URL from the panel and the bearer above, the configuration is the one every Prometheus-compatible tool already takes for a bearer-secured target.
scrape_configs:
- job_name: downpipes
metrics_path: /metrics
scheme: https
static_configs:
- targets: ["console.example.com"]
authorization:
type: Bearer
credentials: "dpc_AbCdEfGhI.dps_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
| Tool | How it reads this endpoint |
|---|---|
| Prometheus | A scrape_configs job, as above. |
| Grafana Agent / Grafana Alloy | A prometheus.scrape component pointed at the same URL and bearer. |
| Grafana Cloud | The hosted, agentless Prometheus scraper (paste the URL and bearer into a scrape job in the Grafana Cloud UI; no agent to run yourself). |
| Datadog Agent | The OpenMetrics check, configured with an openmetrics_endpoint and a bearer token. |
| Dynatrace | An ActiveGate Prometheus extension, or the OpenTelemetry Collector’s prometheusreceiver. |
| New Relic | The Prometheus remote-write integration, or the infrastructure agent’s Prometheus config. |
| Elastic | Elastic Agent’s Prometheus input, or the prometheusreceiver in an OpenTelemetry Collector feeding Elastic. |
| Splunk Observability Cloud | The Splunk Distribution of the OpenTelemetry Collector’s prometheusreceiver. |
Every row above reads the identical wire format the same way, standard Prometheus text exposition needing no downpipes-specific translation, and that much genuinely is Tier 1. What still varies is the effort to point a given tool at it. Grafana Cloud’s hosted scraper is the one fully agentless case here: paste the URL and bearer into its UI and there is nothing else to run. Prometheus, Grafana Agent/Alloy, Elastic’s Prometheus input and the Splunk Distribution of the OpenTelemetry Collector are zero-mapping in the same way, but each still needs its own already-running agent or collector pointed at the endpoint, an ordinary “add a scrape target” step rather than a zero-effort one. Datadog and Dynatrace need genuine configuration: Datadog’s OpenMetrics check needs three configuration fields filled in, low effort but not zero, and Dynatrace’s route needs either a YAML extension mapping file or an OpenTelemetry Collector, plus the ActiveGate or Collector agent itself running. New Relic has no agentless pull at all: reaching it means running the remote-write integration or the infrastructure agent yourself, not pointing an existing hosted scraper at a URL the way Grafana Cloud’s does.
Two field names worth checking before you paste
Datadog’s OpenMetrics check authenticates with a headers map, headers: {Authorization: "Bearer <token>"}, not the bearer_token_auth option, a different and unrelated Datadog Agent setting; the two are easy to confuse by name alone. And where Elastic’s Prometheus input or an OpenTelemetry Collector’s prometheusreceiver offers both bearer_token and bearer_token_file, the two are not interchangeable: bearer_token_file wants a path to a file on disk containing the token, not the token pasted in directly. Use bearer_token for an inline value.
The canonical metric set
Every metric is a gauge, labelled by downpipe_id and downpipe_name (and, for the destination-health series, destination as well). A series is present only when the engine has a real fact to report; it is omitted, never fabricated, when there is nothing yet to say.
| Metric | Meaning | Absent when |
|---|---|---|
downpipe_backup_last_success_timestamp_seconds |
Unix seconds of the most recent successful backup’s completion | No run has ever succeeded |
downpipe_backup_success |
1 if the most recently completed run succeeded, 0 if it failed | No run has completed yet |
downpipe_backup_recent_attempts |
Attempts among the runs currently retained in the history window | Never absent; an honest 0 for a downpipe with no runs yet |
downpipe_backup_recent_successes |
Successes among those same retained runs | Never absent; an honest 0 |
downpipe_backup_recent_failures |
Failures among those same retained runs | Never absent; an honest 0 |
downpipe_backup_duration_seconds |
Wall-clock duration of the most recently completed run | The run predates duration reporting, or none has completed |
downpipe_backup_size_bytes |
Plaintext byte size of the most recent successful backup | No run has succeeded yet |
downpipe_destination_healthy |
1 if the destination’s last seal or replication attempt for this downpipe succeeded, 0 if not | The engine holds no replication state for that destination (for example, the implicit default destination records none by design) |
One metric from the wider canonical set is deliberately absent from this endpoint: downpipe_enabled, which distinguishes a live downpipe from a paused or decommissioned one, is emitted only by OTLP push, not by this scrape surface, in this release. Do not build a dashboard here that assumes it exists on /metrics.
downpipe_name can change under you; downpipe_id cannot
downpipe_name is the operator-chosen label you see in the console, and it is mutable. Renaming a downpipe does not relabel its existing series: the old downpipe_name value stops receiving fresh samples and goes stale, and a new series starts under the new name with no history of its own. A query or an alert rule that keys on downpipe_name and is expected to survive a rename will silently orphan itself the moment someone renames the downpipe. Key any query or alert that needs to survive a rename on downpipe_id instead, which is assigned once and never changes; treat downpipe_name as a display label, not an identifier.
The classic staleness alert
The endpoint exists to make the standard SRE idiom work against your own thresholds, not the engine’s guess at what counts as stale.
time() - downpipe_backup_last_success_timestamp_seconds{downpipe_id="dp_8f2a1"} > 90000
Set the threshold to your own downpipe’s cadence budget plus headroom, not a fixed number across every downpipe: an hourly downpipe and a weekly one need different thresholds, and only you know which is which. Two companion facts make the rule honest rather than merely convenient.
First, the comparison above never fires for a downpipe that has never had a successful run, because an absent series never satisfies a numeric comparison; pair it with absent(downpipe_backup_last_success_timestamp_seconds{downpipe_id="dp_8f2a1"}) if you also want to catch a downpipe that has run zero times. Second, staleness and failure are different conditions worth alerting on separately: downpipe_backup_success == 0 catches a run that concluded and failed, which can be true even while the last-success timestamp is still recent (an older success followed by a newer failure), so a monitoring stack that only watches the staleness threshold can miss a failing downpipe that has not yet gone stale.
Windowed counters, not a lifetime total
downpipe_backup_recent_attempts, _recent_successes and _recent_failures are gauges, not _total counters, and the name says so on purpose. The engine retains a bounded run-history ring per downpipe (50 runs), and these three metrics count only what that ring currently holds. A count that can shrink as old runs age out of the ring would read to rate() or increase() as a counter reset and produce a phantom spike; treating the value as a level rather than a rate is the correct read, and the gauge type is there to stop a consumer applying the wrong idiom to it.
That fixed window is a count of runs, not a span of time, so its wall-clock coverage depends entirely on the downpipe’s own schedule. Fifty retained runs cover roughly the last two days for a downpipe backing up hourly, and roughly the last fifty days for one backing up daily. A single alert threshold applied uniformly across downpipes on different schedules, for example firing whenever recent_failures crosses some fixed count, therefore means a different amount of wall-clock history depending on which downpipe it is evaluated against. Set the threshold with each downpipe’s own schedule in mind, not as one number copied across every downpipe regardless of its cadence.
Injection safety
A downpipe or destination name is an operator-chosen string, so it is treated as untrusted before it reaches a label value. Every label value is escaped for the Prometheus text-exposition grammar (backslash first, then the double quote, then any line break folded to the two-character \n escape), so a name carrying a quote or a newline can never forge a second series or break out of the line it belongs to (escapeLabelValue, engine/src/admin/metrics.ts).
Failure behaviour
A missing or wrong bearer is a plain 401. A method other than GET is a 404, because the surface has no other verb. A genuine backend fault (the scheduler Durable Object is unreachable, or returns something the handler cannot parse) fails the scrape itself with a 500, rather than rendering a healthy-looking empty body: Prometheus’s own per-target up metric is the honest signal for that failure mode, and a fabricated “nothing configured” response would mask a real outage as an empty account. A successful scrape is recorded on the credential’s pull trail, the same best-effort, fire-and-forget recording every ingest credential keeps, so you can confirm from the console when the scrape last actually ran.
Where this fits
For the zero-agent alternative that pushes the same facts out instead of waiting to be scraped, read OTLP metrics push. For the credential mechanism this endpoint shares with vendor diagnostics and the audit feed, read owner-minted pull credentials. For the canonical, honest catalogue of the alert events the same underlying state also drives, read alert events. For the split-topology and Access-perimeter mechanics referenced above, read topology and wiring the audit feed into your SIEM.
Last updated .