# Cloudflare Prometheus Exporter
[](https://github.com/cloudflare/cloudflare-prometheus-exporter)
Export Cloudflare metrics to Prometheus. Built on Cloudflare Workers with Durable Objects for stateful metric accumulation.
[](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/cloudflare-prometheus-exporter)
## Features
- **90+ Prometheus metrics** - requests, bandwidth, threats, workers, load balancers, SSL certs, hostname-level analytics, network analytics, Magic Transit tunnel health/traffic/SLO, Magic Firewall per-rule visibility, stream video/live, and more
- **Cloudflare Workers** - serverless edge deployment
- **Durable Objects** - stateful counter accumulation for proper Prometheus semantics
- **Background refresh** - alarms fetch data every 60s; scrapes return cached data instantly
- **Rate limiting** - 200 req/10s with exponential backoff
- **Multi-account** - automatically discovers and exports all accessible accounts/zones
- **Runtime config API** - change settings without redeployment via REST endpoints
- **Configurable** - zone filtering, metric denylist, label exclusion, custom metrics path, and more
## Quick Start
### One-Click Deploy
Click the deploy button above. Configure `CLOUDFLARE_API_TOKEN` as a secret after deployment. Configure `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD` to protect the exporter with HTTP Basic Auth.
### Manual Deployment
```bash
git clone https://github.com/cloudflare/cloudflare-prometheus-exporter.git
cd cloudflare-prometheus-exporter
bun install
wrangler secret put CLOUDFLARE_API_TOKEN
bun run deploy
```
## Configuration
Configuration is resolved in order: **KV overrides** โ **env vars** โ **defaults**. Use the [Runtime Config API](#runtime-config-api) for dynamic changes without redeployment.
### Environment Variables
Set in `wrangler.jsonc` or via `wrangler secret put`:
| Variable | Default | Description |
|----------|---------|-------------|
| `CLOUDFLARE_API_TOKEN` | - | Cloudflare API token (secret) |
| `QUERY_LIMIT` | 10000 | Max results per GraphQL query |
| `SCRAPE_DELAY_SECONDS` | 300 | Delay before fetching metrics (data propagation) |
| `TIME_WINDOW_SECONDS` | 60 | Query time window |
| `METRIC_REFRESH_INTERVAL_SECONDS` | 60 | Background refresh interval |
| `LOG_LEVEL` | info | Log level (debug/info/warn/error) |
| `LOG_FORMAT` | json | Log format (pretty/json) |
| `ACCOUNT_LIST_CACHE_TTL_SECONDS` | 600 | Account list cache TTL |
| `ZONE_LIST_CACHE_TTL_SECONDS` | 1800 | Zone list cache TTL |
| `SSL_CERTS_CACHE_TTL_SECONDS` | 1800 | SSL cert cache TTL |
| `HEALTH_CHECK_CACHE_TTL_SECONDS` | 10 | Health check cache TTL |
| `EXCLUDE_HOST` | false | Exclude host labels from metrics |
| `CF_HTTP_STATUS_GROUP` | false | Group HTTP status codes (2xx, 4xx, etc.) |
| `PACKED_METRIC_STORAGE` | false | Enable compact storage and chunked output for zone metrics. Accepts the deprecated name `COLO_METRICS_PACKED_STORAGE`. |
| `DISABLE_UI` | false | Disable landing page (returns 404) |
| `DISABLE_CONFIG_API` | false | Disable config API endpoints (returns 404) |
| `METRICS_DENYLIST` | - | Comma-separated list of metrics to exclude |
| `CF_ACCOUNTS` | - | Comma-separated account IDs to include (default: all) |
| `CF_ZONES` | - | Comma-separated zone IDs to include (default: all) |
| `CF_FREE_TIER_ACCOUNTS` | - | Comma-separated account IDs using free tier (skips paid-tier metrics) |
| `HOST_METRICS_ALLOWLIST` | - | Comma-separated hostnames for hostname-level metrics (max 50). Empty disables. Adds 1 extra GraphQL call per account per refresh cycle. `EXCLUDE_HOST=true` also disables. |
| `HOST_METRICS_DELAY_SECONDS` | 60 | Ingestion delay for hostname metrics (seconds). Lower values = fresher data for alerting but risk incomplete data. Independent from `SCRAPE_DELAY_SECONDS`. |
| `METRICS_PATH` | /metrics | Custom path for metrics endpoint |
| `BASIC_AUTH_USER` | - | Username for basic auth (secret, default: no auth, requires `BASIC_AUTH_PASSWORD`) |
| `BASIC_AUTH_PASSWORD` | - | Password for basic auth (secret, default: no auth, requires `BASIC_AUTH_USER`) |
### Creating an API Token
**Quick setup**: [Create token with pre-filled permissions](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22analytics%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22account_analytics%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22workers_scripts%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22ssl_and_certificates%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22firewall_services%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22load_balancers%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22account_logs%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22magic_transit%22%2C%22type%22%3A%22read%22%7D%5D&name=Cloudflare%20Prometheus%20Exporter)
**Manual setup**:
| Permission | Access | Required |
|------------|--------|----------|
| Zone > Analytics | Read | Yes |
| Account > Account Analytics | Read | Yes |
| Account > Workers Scripts | Read | Yes |
| Zone > SSL and Certificates | Read | Optional |
| Zone > Firewall Services | Read | Optional |
| Zone > Load Balancers | Read | Optional |
| Account > Logs | Read | Optional |
| Account > Magic Transit | Read | Optional |
## Endpoints
| Path | Method | Description |
|------|--------|-------------|
| `/` | GET | Landing page (disable: `DISABLE_UI`) |
| `/metrics` | GET | Prometheus metrics |
| `/health` | GET | Health check (`{"status":"healthy"}`) |
| `/config` | GET | Get all runtime config (disable: `DISABLE_CONFIG_API`) |
| `/config` | DELETE | Reset all config to env defaults (disable: `DISABLE_CONFIG_API`) |
| `/config/:key` | GET | Get single config value (disable: `DISABLE_CONFIG_API`) |
| `/config/:key` | PUT | Set config override (persisted in KV) (disable: `DISABLE_CONFIG_API`) |
| `/config/:key` | DELETE | Reset config key to env default (disable: `DISABLE_CONFIG_API`) |
## Prometheus Configuration
```yaml
scrape_configs:
- job_name: 'cloudflare'
scrape_interval: 60s
scrape_timeout: 30s
static_configs:
- targets: ['your-worker.your-subdomain.workers.dev']
```
### With Basic Auth
Set up basic auth to protect all endpoints:
```bash
wrangler secret put BASIC_AUTH_USER
wrangler secret put BASIC_AUTH_PASSWORD
```
Then configure Prometheus:
```yaml
scrape_configs:
- job_name: 'cloudflare'
scrape_interval: 60s
scrape_timeout: 30s
basic_auth:
username: 'your-username'
password: 'your-password'
static_configs:
- targets: ['your-worker.your-subdomain.workers.dev']
```
## Runtime Config API
Override configuration at runtime without redeployment. Overrides persist in KV and take precedence over `wrangler.jsonc` env vars.
### Config Keys
| Key | Type | Description |
|-----|------|-------------|
| `queryLimit` | number | Max results per GraphQL query |
| `scrapeDelaySeconds` | number | Delay before fetching metrics |
| `timeWindowSeconds` | number | Query time window |
| `metricRefreshIntervalSeconds` | number | Background refresh interval |
| `accountListCacheTtlSeconds` | number | Account list cache TTL |
| `zoneListCacheTtlSeconds` | number | Zone list cache TTL |
| `sslCertsCacheTtlSeconds` | number | SSL cert cache TTL |
| `healthCheckCacheTtlSeconds` | number | Health check cache TTL |
| `logFormat` | `"json"` \| `"pretty"` | Log format |
| `logLevel` | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | Log level |
| `cfAccounts` | string \| null | Comma-separated account IDs (null = all) |
| `cfZones` | string \| null | Comma-separated zone IDs (null = all) |
| `cfFreeTierAccounts` | string | Comma-separated free tier account IDs |
| `metricsDenylist` | string | Comma-separated metrics to exclude |
| `excludeHost` | boolean | Exclude host labels |
| `httpStatusGroup` | boolean | Group HTTP status codes |
| `packedMetricStorage` | boolean | Enable compact by-zone storage and chunked read output for supported high-cardinality queries, including edge-country, health-check, and hostname HTTP metrics |
| `coloMetricsPackedStorage` | boolean | Deprecated alias for `packedMetricStorage` |
| `hostMetricsAllowlist` | string | Comma-separated hostnames for hostname-level metrics |
| `hostMetricsDelaySeconds` | number | Ingestion delay for hostname metrics (seconds) |
### Examples
```bash
# Get all config
curl https://your-worker.workers.dev/config
# Get single value
curl https://your-worker.workers.dev/config/logLevel
# Set override
curl -X PUT https://your-worker.workers.dev/config/logLevel \
-H "Content-Type: application/json" \
-d '{"value": "debug"}'
# Filter to specific zones
curl -X PUT https://your-worker.workers.dev/config/cfZones \
-H "Content-Type: application/json" \
-d '{"value": "zone-id-1,zone-id-2"}'
# Enable compact packed metric storage for trial rollout
curl -X PUT https://your-worker.workers.dev/config/packedMetricStorage \
-H "Content-Type: application/json" \
-d '{"value": true}'
# Reset to env default
curl -X DELETE https://your-worker.workers.dev/config/logLevel
# Reset all overrides
curl -X DELETE https://your-worker.workers.dev/config
```
### Packed metric storage
`packedMetricStorage` stores selected queries in compact typed state with named label columns instead of label-repeating rows, and streams the output in bounded chunks. Metric names and labels are unchanged.
- Enabling packed storage migrates active and retained counters before adding the next window. Disabling deletes packed snapshots and starts a fresh unpacked counter generation on the next refresh.
- The storage mode is resolved once per scrape and applied to every account, so a scrape never mixes packed and unpacked output for the same metric family.
- Each counter series stores its own retry checkpoint. Replaying the same query window is idempotent. Series not seen for five refreshes are dropped; until then they are exported with their last value (a flat counter), whereas the unpacked path stops exporting a series the moment it is absent.
- When `excludeHost` is set, packed rows that collapse onto the same remaining labels are summed, matching the unpacked serializer.
- Packed queries keep their existing metric families. `colo-metrics` uses its reduced packed query, health checks use separate event and timing aliases, and hostname metrics preserve their four aliases and fixed one-minute window.
- The 16 MiB serialized-state guard (`Chunked storage value exceeds the safe size limit`) still applies per query. Families with identical label columns share one copy, keeping high-cardinality multi-family queries such as `colo-metrics` within the guard.
## Available Metrics
### Zone Request Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_requests_total` | counter | zone |
| `cloudflare_zone_requests_cached` | gauge | zone |
| `cloudflare_zone_requests_ssl_encrypted_total` | counter | zone |
| `cloudflare_zone_requests_content_type_total` | counter | zone, content_type |
| `cloudflare_zone_requests_country_total` | counter | zone, country |
| `cloudflare_zone_requests_status_total` | counter | zone, status |
| `cloudflare_zone_requests_browser_map_page_views_total` | counter | zone, family |
| `cloudflare_zone_requests_ip_class_total` | counter | zone, ip_class |
| `cloudflare_zone_requests_ssl_protocol_total` | counter | zone, ssl_protocol |
| `cloudflare_zone_requests_http_version_total` | counter | zone, http_version |
| `cloudflare_zone_requests_origin_status_country_host_total` | counter | zone, origin_status, country, host |
| `cloudflare_zone_requests_status_country_host_total` | counter | zone, edge_status, country, host |
| `cloudflare_zone_requests_by_method_total` | counter | zone, method |
### Zone Bandwidth Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_bandwidth_total` | counter | zone |
| `cloudflare_zone_bandwidth_cached_total` | counter | zone |
| `cloudflare_zone_bandwidth_ssl_encrypted_total` | counter | zone |
| `cloudflare_zone_bandwidth_content_type_total` | counter | zone, content_type |
| `cloudflare_zone_bandwidth_country_total` | counter | zone, country |
### Zone Threat Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_threats_total` | counter | zone |
| `cloudflare_zone_threats_country_total` | counter | zone, country |
| `cloudflare_zone_threats_type_total` | counter | zone, type |
### Zone Page/Unique Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_pageviews_total` | counter | zone |
| `cloudflare_zone_uniques_total` | counter | zone |
### Colocation Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_colocation_visits_total` | counter | zone, colo, host |
| `cloudflare_zone_colocation_edge_response_bytes_total` | counter | zone, colo, host |
| `cloudflare_zone_colocation_requests_total` | counter | zone, colo, host |
| `cloudflare_zone_colocation_error_visits_total` | counter | zone, colo, host, status |
| `cloudflare_zone_colocation_error_edge_response_bytes_total` | counter | zone, colo, host, status |
| `cloudflare_zone_colocation_error_requests_total` | counter | zone, colo, host, status |
### Firewall Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_firewall_events_total` | counter | zone, action, source, rule, host, country |
| `cloudflare_zone_firewall_bots_detected_total` | counter | zone, bot_score, detection_source |
### Health Check Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_health_check_events_origin_total` | counter | zone, health_status, origin_ip, region, fqdn, failure_reason |
| `cloudflare_zone_health_check_events_avg` | gauge | zone |
| `cloudflare_zone_health_check_rtt_seconds` | gauge | zone, origin_ip, fqdn |
| `cloudflare_zone_health_check_ttfb_seconds` | gauge | zone, origin_ip, fqdn |
| `cloudflare_zone_health_check_tcp_connection_seconds` | gauge | zone, origin_ip, fqdn |
| `cloudflare_zone_health_check_tls_handshake_seconds` | gauge | zone, origin_ip, fqdn |
### Worker Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_worker_requests_total` | counter | script_name |
| `cloudflare_worker_errors_total` | counter | script_name |
| `cloudflare_worker_cpu_time_seconds` | gauge | script_name, quantile |
| `cloudflare_worker_duration_seconds` | gauge | script_name, quantile |
### Load Balancer Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_pool_health_status` | gauge | zone, lb_name, pool_name |
| `cloudflare_zone_pool_requests_total` | counter | zone, lb_name, pool_name, origin_name |
| `cloudflare_zone_lb_pool_rtt_seconds` | gauge | zone, lb_name, pool_name |
| `cloudflare_zone_lb_steering_policy_info` | gauge | zone, lb_name, policy |
| `cloudflare_zone_lb_origins_selected_count` | gauge | zone, lb_name, pool_name |
| `cloudflare_zone_lb_origin_weight` | gauge | zone, lb_name, pool_name, origin_name |
### Logpush Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_logpush_failed_jobs_account_total` | counter | account, job_id, status, destination_type |
| `cloudflare_logpush_failed_jobs_zone_total` | counter | zone, job_id, destination_type |
### Error Rate Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_customer_error_4xx_total` | counter | zone, status, country, host |
| `cloudflare_zone_customer_error_5xx_total` | counter | zone, status, country, host |
| `cloudflare_zone_edge_error_rate` | gauge | zone |
| `cloudflare_zone_origin_error_rate` | gauge | zone |
| `cloudflare_zone_origin_response_duration_seconds` | gauge | zone, status, country, host |
### Cache Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_cache_hit_ratio` | gauge | zone |
| `cloudflare_zone_cache_miss_origin_duration_seconds` | gauge | zone, country, host |
### Bot Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_bot_requests_by_country_total` | counter | zone, country |
### Magic Transit Metrics
**Tunnel Health** (from `magic-transit` query)
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_magic_transit_healthy_tunnels` | gauge | account, tunnel_name, site_name |
| `cloudflare_magic_transit_tunnel_failures` | gauge | account, tunnel_name, site_name |
| `cloudflare_magic_transit_edge_colo_count` | gauge | account, tunnel_name, site_name |
| `cloudflare_magic_transit_tunnel_failure_by_status` | gauge | account, tunnel_name, site_name, result_status |
| `cloudflare_magic_transit_tunnel_state_healthy` | gauge | account, tunnel_name, site_name |
| `cloudflare_magic_transit_tunnel_state_degraded` | gauge | account, tunnel_name, site_name |
| `cloudflare_magic_transit_tunnel_state_down` | gauge | account, tunnel_name, site_name |
> Tunnel state gauges follow the StateSet pattern: exactly one of `_healthy`, `_degraded`, `_down` is 1 per tunnel, the others are 0. Derived from Cloudflare health check scores (weighted average thresholded at 0.75/0.25).
**Tunnel SLO** (from `magic-transit-slo` query)
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_magic_transit_tunnel_slo_status` | gauge | account, tunnel_name, site_name, status |
| `cloudflare_magic_transit_tunnel_effective_slo` | gauge | account, tunnel_name, site_name |
| `cloudflare_magic_transit_tunnel_target_slo` | gauge | account, tunnel_name, site_name |
**Tunnel Traffic** (from `magic-transit-traffic` query)
Per-tunnel bandwidth and packet counters with ramp method visibility.
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_magic_transit_tunnel_traffic_bits_total` | counter | account, tunnel_name, direction, on_ramp, off_ramp |
| `cloudflare_magic_transit_tunnel_traffic_packets_total` | counter | account, tunnel_name, direction, on_ramp, off_ramp |
### Magic Firewall Metrics
**Per-Rule Sampled Traffic** (from `magic-firewall-samples` query)
Traffic allowed and blocked by specific Magic Firewall rules.
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_magic_firewall_rule_bits_total` | counter | account, rule_id |
| `cloudflare_magic_firewall_rule_packets_total` | counter | account, rule_id |
### Network Analytics Metrics
Traffic volume metrics across Cloudflare's Network Analytics v2 datasets. All are account-level counters with low-cardinality labels. Datasets that don't apply to an account (e.g. no Magic Transit subscription) return no data.
**Magic Transit**
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_network_analytics_magic_transit_bits_total` | counter | account, outcome, direction, ip_protocol, mitigation_system |
| `cloudflare_network_analytics_magic_transit_packets_total` | counter | account, outcome, direction, ip_protocol, mitigation_system |
**Magic Firewall**
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_network_analytics_magic_firewall_bits_total` | counter | account, outcome, direction, ip_protocol |
| `cloudflare_network_analytics_magic_firewall_packets_total` | counter | account, outcome, direction, ip_protocol |
**DDoS Defense (dosd)**
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_network_analytics_dosd_bits_total` | counter | account, outcome, direction, ip_protocol, attack_vector |
| `cloudflare_network_analytics_dosd_packets_total` | counter | account, outcome, direction, ip_protocol, attack_vector |
**Intrusion Detection (IDPS)**
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_network_analytics_idps_bits_total` | counter | account, outcome, direction, ip_protocol |
| `cloudflare_network_analytics_idps_packets_total` | counter | account, outcome, direction, ip_protocol |
**Advanced TCP Protection**
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_network_analytics_tcp_protection_bits_total` | counter | account, outcome, direction, ip_protocol |
| `cloudflare_network_analytics_tcp_protection_packets_total` | counter | account, outcome, direction, ip_protocol |
**Advanced DNS Protection**
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_network_analytics_dns_protection_bits_total` | counter | account, outcome, direction, ip_protocol |
| `cloudflare_network_analytics_dns_protection_packets_total` | counter | account, outcome, direction, ip_protocol |
### Stream Video Playback Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_stream_video_playback_count_total` | counter | account, country, media_type |
| `cloudflare_stream_video_playback_time_viewed_seconds_total` | counter | account, country, media_type |
### Stream Live Input Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_stream_live_input_segments_total` | counter | account, event_code |
| `cloudflare_stream_live_input_bit_rate_bps` | gauge | account, event_code |
| `cloudflare_stream_live_input_gop_duration_seconds` | gauge | account, event_code |
| `cloudflare_stream_live_input_upload_duration_ratio` | gauge | account, event_code |
### Hostname Metrics
Requires `HOST_METRICS_ALLOWLIST` to be set (max 50 hostnames). Disabled when `EXCLUDE_HOST=true`.
All hostname metrics are **gauge snapshots** of the **last completed minute**, designed for alerting. The ingestion delay is controlled by `HOST_METRICS_DELAY_SECONDS` (default: 60s) independently from the global `SCRAPE_DELAY_SECONDS`. Hosts with zero traffic in the minute will not emit series.
**Request counts:**
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `cloudflare_zone_hostname_requests` | gauge | zone, host | Total requests in the last completed minute |
| `cloudflare_zone_hostname_requests_by_status` | gauge | zone, host, status | Requests by HTTP status code |
| `cloudflare_zone_hostname_cache_status` | gauge | zone, host, cache_status | Requests by cache status (hit/miss/etc.) |
**Latency (averages โ primary alerting metrics):**
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `cloudflare_zone_hostname_edge_ttfb_seconds` | gauge | zone, host | Average edge TTFB in seconds |
| `cloudflare_zone_hostname_origin_response_duration_seconds` | gauge | zone, host | Average origin response duration in seconds |
**Latency (percentiles โ separate metric families):**
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `cloudflare_zone_hostname_edge_ttfb_p50_seconds` | gauge | zone, host | P50 edge TTFB in seconds |
| `cloudflare_zone_hostname_edge_ttfb_p95_seconds` | gauge | zone, host | P95 edge TTFB in seconds |
| `cloudflare_zone_hostname_origin_response_duration_p50_seconds` | gauge | zone, host | P50 origin response duration in seconds |
| `cloudflare_zone_hostname_origin_response_duration_p95_seconds` | gauge | zone, host | P95 origin response duration in seconds |
### SSL Certificate Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_zone_certificate_validation_status` | gauge | zone, type, issuer, status |
### Exporter Info Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `cloudflare_exporter_up` | gauge | - |
| `cloudflare_exporter_errors_total` | counter | account_id, error_code |
| `cloudflare_accounts` | gauge | - |
| `cloudflare_zones` | gauge | - |
| `cloudflare_zones_filtered` | gauge | - |
| `cloudflare_zones_processed` | gauge | - |
| `cloudflare_zones_skipped_free_tier` | gauge | - |
## Free Tier Zone Limitations
Zones on Cloudflare's Free plan don't have access to the GraphQL Analytics API. The exporter automatically detects and skips free tier zones for metrics that require this API.
**Free tier zones still export:**
- `cloudflare_zone_certificate_validation_status` (SSL certificates)
- `cloudflare_zone_lb_origin_weight` (Load balancer weights, if configured)
**Monitor skipped zones:**
```
cloudflare_zones_skipped_free_tier
```
For mixed accounts (enterprise + free zones), only free zones are skippedโpaid zones continue to export all metrics.
## Architecture
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ WORKER ISOLATE โ
โ โโโโโโโโโโโโโโโโโโ โ
โ โ Worker.fetch โโโโโ HTTP /metrics, /health, /config โ
โ โ (HTTP handler) โ โ
โ โโโโโโโโโฌโโโโโโโโโ โ
โ โ โ
โ โ RPC (stub.export()) โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ CONFIG_KV: Runtime config overrides (merged with env defaults) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DURABLE OBJECT ISOLATES โ
โ โ
โ Each DO runs in its own V8 isolate with: โ
โ - Own CloudflareMetricsClient instance (per-isolate singleton) โ
โ - Own persistent storage โ
โ - Own alarm scheduler โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ MetricCoordinator (1 global instance) โ โ
โ โ ID: "metric-coordinator" โ โ
โ โ State: accounts[], lastAccountFetch โ โ
โ โ Cache TTL: 600s (account list) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ RPC โ
โ โโโโโโโโโโโโโโผโโโโโโโโโโโโโ โ
โ โผ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ AccountMetric โ โ AccountMetric โ โ AccountMetric โ โ
โ โ Coordinator โ โ Coordinator โ โ Coordinator โ โ
โ โ account:acct1 โ โ account:acct2 โ โ account:acct3 โ โ
โ โ Alarm: 60s โ โ Alarm: 60s โ โ Alarm: 60s โ โ
โ โ Zone TTL: 1800s โ โ Zone TTL: 1800s โ โ Zone TTL: 1800s โ โ
โ โโโโโโโโโฌโโโโโโโโโโ โโโโโโโโโฌโโโโโโโโโโ โโโโโโโโโฌโโโโโโโโโโ โ
โ โ RPC โ โ โ
โ โโโโโโโโดโโโโโโ โโโโโโโโดโโโโโโ โโโโโโโโดโโโโโโ โ
โ โผ โผ โผ โผ โผ โผ โ
โ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โ
โ โExprtโ โExprtโ โExprtโ โExprtโ โExprtโ โExprtโ โ
โ โ(21) โ .. โ(N) โ โ(21) โ .. โ(N) โ โ(21) โ .. โ(N) โ โ
โ โacct โ โzone โ โacct โ โzone โ โacct โ โzone โ โ
โ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โ
โ โ
โ MetricExporter DOs (per account): โ
โ - Account-scoped (21): worker-totals, logpush-account, magic-transit, โ
โ magic-transit-slo, magic-transit-traffic, magic-firewall-samples, โ
โ network-analytics, stream-video-playback, stream-live-inputs, โ
โ http-metrics, adaptive-metrics, edge-country-metrics, โ
โ colo-metrics, colo-error-metrics, request-method-metrics, โ
โ health-check-metrics, load-balancer-metrics, logpush-zone, โ
โ origin-status-metrics, cache-miss-metrics, hostname-http-metrics โ
โ - Zone-scoped (N per account, 1 per zone): ssl-certificates, lb-weight-metrics โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ CloudflareMetricsClient (per-isolate) โ โ
โ โ - urql Client (GraphQL) โ โ
โ โ - Cloudflare SDK (REST) โ โ
โ โ - DataLoader: firewallRulesLoader (batches Promise.all calls) โ โ
โ โ - Global Rate limiter: 200 req/10s with exponential backoff โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
### Request Path: Prometheus Scrape (GET /metrics)
```
โโโโโโโโโโโโ GET /metrics โโโโโโโโโโ
โPrometheusโโโโโโโโโโโโโโโโโโถโ Worker โ
โ Server โ โ .fetch โ
โโโโโโโโโโโโ โโโโโฌโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโ
โ MetricCoordinator โ
โ โ
โ 1. Check account cache (TTL: 600s) โ
โ 2. If stale โ getAccounts() โ
โ 3. Fan out to AccountMetricCoordinators โ
โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
โ AccountMetric โ โ AccountMetric โ โ AccountMetric โ
โ Coordinator โ โ Coordinator โ โ Coordinator โ
โ (Account A) โ โ (Account B) โ โ (Account C) โ
โ โ โ โ โ โ
โ 1. Check if โ โ โ โ โ
โ refresh() โ โ (parallel) โ โ (parallel) โ
โ needed โ โ โ โ โ
โ 2. Fan out to โ โ โ โ โ
โ exporters โ โ โ โ โ
โโโโโโโโโฌโโโโโโโโโ โโโโโโโโโฌโโโโโโโโโ โโโโโโโโโฌโโโโโโโโโ
โ โ โ
โโโโโโโดโโโโโโ โโโโโโโดโโโโโโ โโโโโโโดโโโโโโ
โผ โผ โผ โผ โผ โผ
โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ
โExprtโ...โExprtโ โExprtโ...โExprtโ โExprtโ...โExprtโ
โ21+N โ โ โ โ21+N โ โ โ โ21+N โ โ โ
โ โ โ โ โ โ โ โ โ โ โ โ
โ ret โ โ ret โ โ ret โ โ ret โ โ ret โ โ ret โ
โcacheโ โcacheโ โcacheโ โcacheโ โcacheโ โcacheโ
โโโโฌโโโ โโโโฌโโโ โโโโฌโโโ โโโโฌโโโ โโโโฌโโโ โโโโฌโโโ
โ โ โ โ โ โ
โโโโโโฌโโโโโ โโโโโโฌโโโโโ โโโโโโฌโโโโโ
โ โ โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโ
โ FAN-IN: Merge โ
โ all metrics + โ
โ serialize to โ
โ Prometheus fmt โ
โโโโโโโโโโฌโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโ
โ HTTP Response โ
โ text/plain โ
โโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NOTE: Request path is FAST - just reads cached metrics โ
โ No network calls to Cloudflare API during scrape โ
โ (unless account list cache is stale) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
### Background Refresh Path: Alarm-Driven Metric Fetching
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ALARM TRIGGERS โ
โ AccountMetricCoordinator: every 60s โ
โ MetricExporter: every 60s + 1-5s fixed jitterโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
**AccountMetricCoordinator.alarm()**
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AccountMetricCoordinator.refresh() โ
โ โ
โ 1. Check zone cache (TTL: 1800s / 30 min) โ
โ โ
โ 2. If stale: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ REST: getZones(accountId) โ โ
โ โ โโโบ DataLoader batches if multiple calls same tick โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ REST: getFirewallRules(zoneId) ร N zones (parallel) โ โ
โ โ โโโบ DataLoader batches parallel calls โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ 3. Push context to MetricExporter DOs: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Account-scoped (21 exporters): โ โ
โ โ exporter.updateZoneContext(accountId, accountName, zones) โ โ
โ โ โ โ
โ โ Zone-scoped (N exporters, 1 per zone): โ โ
โ โ exporter.initializeZone(zone, accountId, accountName) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ 4. Schedule next alarm (60s) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
**MetricExporter.alarm()**
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MetricExporter.refresh() for account-scoped queries โ
โ โ
โ Query Types (21 total): โ
โ โโโ ACCOUNT-LEVEL (single account per query, 9): โ
โ โ โโโ worker-totals โ
โ โ โโโ logpush-account โ
โ โ โโโ magic-transit โ
โ โ โโโ magic-transit-slo โ
โ โ โโโ magic-transit-traffic โ
โ โ โโโ magic-firewall-samples โ
โ โ โโโ network-analytics โ
โ โ โโโ stream-video-playback โ
โ โ โโโ stream-live-inputs โ
โ โ โ
โ โโโ ZONE-LEVEL (all zones batched in one query, 12): โ
โ โโโ http-metrics โ
โ โโโ adaptive-metrics โ
โ โโโ edge-country-metrics โ
โ โโโ colo-metrics โ
โ โโโ colo-error-metrics โ
โ โโโ request-method-metrics โ
โ โโโ health-check-metrics โ
โ โโโ load-balancer-metrics โ
โ โโโ logpush-zone โ
โ โโโ origin-status-metrics โ
โ โโโ cache-miss-metrics โ
โ โโโ hostname-http-metrics โ
โ โ
โ After fetch: Process counters โ Cache metrics โ Schedule next alarm โ
โ Jitter: 1-5s fixed (tighter clustering for time range alignment) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
Counter series are retained for five missed refreshes before being pruned. Metric
exporter snapshots are persisted in bounded chunks (with a 16 MiB total safety
limit), product queries denied by Cloudflare are retried hourly, and unexpected
alarm failures schedule a recovery alarm so metric refreshes cannot silently stop.
Chunked snapshots retain the last unchunked state at the legacy storage key, but a
rollback to a pre-chunking release cannot read the current chunked snapshot and may
resume the original oversized-write failure. Prefer a forward fix after deployment.
## Development
```bash
bun install # Install dependencies
bun run dev # Run locally (port 8787)
bun run check # Lint + format check
bun run test # Run tests
bun run typecheck # Type-check without emitting files
bun run deploy # Deploy to Cloudflare
```
## Tech Stack
- **[Hono](https://hono.dev/)** - Web framework
- **[urql](https://formidable.com/open-source/urql/)** - GraphQL client
- **[gql.tada](https://gql-tada.0no.co/)** - Type-safe GraphQL
- **[Zod](https://zod.dev/)** - Schema validation
- **[DataLoader](https://github.com/graphql/dataloader)** - Request batching
- **[Cloudflare SDK](https://developers.cloudflare.com/api/)** - REST API client
- **[Cloudflare KV](https://developers.cloudflare.com/kv/)** - Runtime config persistence
## License
MIT