Cloudflare GraphQL Analytics API: A Practical Guide
Every chart in the Cloudflare dashboard is powered by one API: the GraphQL Analytics API. If you want your data in a script, a spreadsheet, or your own dashboard, this is where it comes from.
This guide covers what you need to be productive: authentication, datasets, limits, sampling, and three queries you can run today.
The endpoint and authentication
There is a single endpoint for every query:
POST https://api.cloudflare.com/client/v4/graphql
Authenticate with an API token (not the legacy Global API Key). Create one at My Profile → API Tokens with read-only analytics permission:
- Zone → Analytics → Read for zone-level datasets (traffic, security events)
- Account → Account Analytics → Read for account-level datasets
A minimal request looks like this:
curl -s https://api.cloudflare.com/client/v4/graphql \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"query": "{ viewer { zones(filter: {zoneTag: \"'"$ZONE_ID"'\"}) { zoneTag } } }"}'
If that returns your zone ID, your token works.
How datasets are named
Datasets live under viewer → zones or viewer → accounts. The names follow a pattern that tells you what you’re getting:
| Suffix | Meaning | Example |
|---|---|---|
1dGroups |
Pre-aggregated daily rollups | httpRequests1dGroups |
1hGroups |
Pre-aggregated hourly rollups | httpRequests1hGroups |
Adaptive |
Sampled raw events | firewallEventsAdaptive |
AdaptiveGroups |
Sampled events, grouped by dimensions you choose | httpRequestsAdaptiveGroups |
Groups datasets return aggregates: count, sum { ... }, uniq { ... }, grouped by the dimensions you ask for. Non-group datasets return individual events.
Limits: check them before you build
Every dataset has limits that depend on your plan:
notOlderThan— how far back you can query (your retention limit)maxDuration— the widest time range per querymaxPageSize— the most rows per response
Query them from the settings node. We walk through it in how to check your exact Cloudflare analytics retention limits.
Common gotcha: a query that works for “last 24 hours” fails for “last 30 days” because it exceeds
maxDuration. Split long ranges into windows.
Sampling: know when numbers are estimates
Datasets with the Adaptive suffix use adaptive sampling. At low volume you see everything. As volume grows, Cloudflare samples at progressively lower rates so queries return in seconds.
For trends and top-N lists, that’s fine. For exact counts, prefer the 1dGroups and 1hGroups rollups, or raw logs on Enterprise.
Query 1: Daily traffic for the last week
query ($zoneTag: string, $start: Date, $end: Date) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
httpRequests1dGroups(
limit: 7
filter: { date_geq: $start, date_lt: $end }
orderBy: [date_ASC]
) {
dimensions { date }
sum { requests bytes cachedRequests cachedBytes threats pageViews }
uniq { uniques }
}
}
}
}
Variables: {"zoneTag": "...", "start": "2026-06-26", "end": "2026-07-03"}.
Query 2: Top paths by bandwidth
This one is adapted from Cloudflare’s own tutorial:
query ($zoneTag: string, $start: Time, $end: Time) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
httpRequestsAdaptiveGroups(
limit: 10
filter: { datetime_geq: $start, datetime_lt: $end }
orderBy: [sum_edgeResponseBytes_DESC]
) {
count
sum { edgeResponseBytes }
dimensions { clientRequestPath }
}
}
}
}
Note the Time type: adaptive datasets filter on datetime (ISO 8601 timestamps like 2026-07-02T00:00:00Z), while daily rollups filter on date.
Query 3: Most recent security events
query ($zoneTag: string, $start: Time, $end: Time) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
firewallEventsAdaptive(
limit: 20
filter: { datetime_geq: $start, datetime_lt: $end }
orderBy: [datetime_DESC]
) {
datetime
action
source
clientIP
clientCountryName
clientRequestPath
userAgent
}
}
}
}
Remember that Security Events only go back 24 hours on Free and Pro.
Handling errors
GraphQL returns HTTP 200 even when a query fails. Always check the errors array in the response body:
... | jq 'if .errors then .errors else .data end'
The usual errors are a time range that’s too old (notOlderThan), too wide (maxDuration), or a field your plan can’t access (availableFields).
From queries to history
The API only returns what’s inside your retention window. To build history, run these queries on a schedule and store the results. See archive Cloudflare analytics to R2 with a Worker or export Cloudflare analytics to CSV.
Or let MetricKeeper do it: connect a read-only token and it archives these datasets for you, with a dashboard on top.
Sources
- Cloudflare: GraphQL Analytics API
- Cloudflare: Querying HTTP events by hostname
- Cloudflare: Querying firewall events
- Cloudflare: Sampling