Cloudflare GraphQL Analytics API -- Custom Queries
In this tutorial, you'll learn about Cloudflare GraphQL Analytics API. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Cloudflare GraphQL Analytics API lets you query your zone, account, and cache analytics using standard GraphQL operations, enabling you to build custom dashboards, automate reporting, and extract precisely the metrics you need without relying on pre-built dashboard views.
Why GraphQL Analytics Matters
The Cloudflare dashboard provides excellent out-of-the-box analytics, but you cannot easily combine metrics, filter by custom dimensions, or export data at scale from the UI. The GraphQL API solves this by exposing a rich schema of datasets -- HTTP requests, bandwidth, firewall events, bot management scores, cache status, and more -- all queryable with a single POST request. You can join datasets, aggregate over arbitrary time ranges, and paginate through millions of events. This is the same API that powers Cloudflare's own analytics dashboards, so you know it is complete and up to date.
Real-world use: A SaaS company serving 10 million monthly visitors needs to track cache hit ratio per country over the last 30 days. They query the httpRequests1mGroups dataset with a filter on clientCountryName, group by cacheStatus, and pipe the results into a Grafana dashboard. The entire data pipeline runs on a cron job with no human intervention.
GraphQL Schema Overview
The Analytics API is organized around datasets that represent aggregated views of your data. Each dataset is a GraphQL type with fields you can select and filter.
flowchart TD
Q[GraphQL Query] --> S[Cloudflare GraphQL Endpoint]
S --> V[Schema Validation]
V --> D[Dataset Resolver]
D --> Z1["HTTP Requests
httpRequests1mGroups"]
D --> Z2["Firewall Events
firewallEventsAdaptiveGroups"]
D --> Z3["Cache Analytics
cacheAnalyticsAdaptiveGroups"]
D --> Z4["Bot Management
botManagementAdaptiveGroups"]
Z1 --> R1[Aggregated Results]
Z2 --> R1
Z3 --> R1
Z4 --> R1
R1 --> J[JSON Response]
style S fill:#f90,color:#fff
style D fill:#f90,color:#fff
Each dataset comes in two granularities: AdaptiveGroups (raw or near-real-time) and 1mGroups / 1hGroups (time-bucketed aggregates). Choose based on whether you need per-event detail or bucketed summaries.
Authentication
Every GraphQL request requires an API token with the Analytics:read permission. You create this token in the Cloudflare dashboard under My Profile > API Tokens.
# Set your API token and account/zone identifiers
export CLOUDFLARE_API_TOKEN="your-api-token-here"
export CLOUDFLARE_ACCOUNT_ID="your-account-id"
export CLOUDFLARE_ZONE_ID="your-zone-id"
The token must be passed as a Bearer token in the Authorization header. You can verify your token works by querying the schema endpoint before running real queries.
Query 1: HTTP Request Count by Status Code
This is the most common starting point -- get total requests grouped by HTTP status code for the last 24 hours.
curl -s -X POST "https://api.cloudflare.com/client/v4/graphql" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"query": "{
viewer {
zones(filter: {zoneTag: $ZONE_ID}) {
httpRequests1mGroups(
limit: 100
filter: {datetime_gt: \"2026-06-22T00:00:00Z\"}
orderBy: [datetime_DESC]
) {
dimensions {
datetime
edgeResponseStatus
}
sum {
requests
bytes
}
}
}
}
}",
"variables": {
"ZONE_ID": "'$CLOUDFLARE_ZONE_ID'"
}
}' | python3 -m json.tool
Expected output: A JSON object with data.viewer.zones[0].httpRequests1mGroups containing an array of records, each with dimensions.datetime, dimensions.edgeResponseStatus, sum.requests, and sum.bytes. You will see status codes like 200, 301, 404, 503 with their respective counts.
This query uses the httpRequests1mGroups dataset aggregated per minute. The datetime_gt filter sets the start time, and results are ordered newest first. Each group contains the status code and the sum of requests and bytes for that minute.
Query 2: Cache Hit Ratio per Country
Cache hit ratio is one of the most important performance metrics. This query breaks it down by client country.
curl -s -X POST "https://api.cloudflare.com/client/v4/graphql" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"query": "{
viewer {
zones(filter: {zoneTag: $ZONE_ID}) {
httpRequests1mGroups(
limit: 200
filter: {datetime_gt: \"2026-06-22T00:00:00Z\", AND: {cacheStatus: \"hit\"}}
) {
dimensions {
clientCountryName
cacheStatus
}
count
}
}
}
}",
"variables": {
"ZONE_ID": "'$CLOUDFLARE_ZONE_ID'"
}
}' | python3 -m json.tool
Expected output: A list of country names with cacheStatus set to "hit" and a count column. Countries with the highest hit ratios indicate well-cached traffic, while countries with low or no hits may need cache rule tuning or have region-specific uncacheable content.
To compute the actual hit ratio, you need two queries -- one for total requests per country and one for cache hits -- then divide hits by total. Alternatively, use the cacheAnalyticsAdaptiveGroups dataset which provides hit ratio directly.
Query 3: Top 10 URLs by Bandwidth
Identify which URLs consume the most bandwidth on your zone.
curl -s -X POST "https://api.cloudflare.com/client/v4/graphql" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"query": "{
viewer {
zones(filter: {zoneTag: $ZONE_ID}) {
httpRequestsAdaptiveGroups(
limit: 10
filter: {datetime_gt: \"2026-06-22T00:00:00Z\"}
orderBy: [sum_bytes_DESC]
) {
dimensions {
clientRequestPath
}
sum {
bytes
}
avg {
sampleInterval
}
}
}
}
}",
"variables": {
"ZONE_ID": "'$CLOUDFLARE_ZONE_ID'"
}
}' | python3 -m json.tool
Expected output: The top 10 URL paths sorted by total bandwidth descending. The largest paths often include large assets like videos, PDFs, high-resolution images, or unoptimized JavaScript bundles. Use this data to prioritize optimization efforts -- enable Image Optimization, adjust cache rules, or move large files to Cloudflare R2 object storage.
This query uses the adaptive (non-bucketed) dataset for more granular results. The orderBy parameter sorts by bytes in descending order, and limit: 10 returns only the top entries.
Query 4: Firewall Events by Action and Source
Monitor blocked and challenged traffic to understand attack patterns.
curl -s -X POST "https://api.cloudflare.com/client/v4/graphql" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"query": "{
viewer {
zones(filter: {zoneTag: $ZONE_ID}) {
firewallEventsAdaptiveGroups(
limit: 25
filter: {datetime_gt: \"2026-06-22T00:00:00Z\", AND: {action: \"block\"}}
) {
dimensions {
action
clientCountryName
source
edgeResponseStatus
}
count
}
}
}
}",
"variables": {
"ZONE_ID": "'$CLOUDFLARE_ZONE_ID'"
}
}' | python3 -m json.tool
Expected output: Blocked firewall events grouped by action type, source country, source rule (e.g. waf, rateLimit, ipAccess), and HTTP status code. A high volume of blocks from a specific country or source suggests targeted attack patterns that may warrant a custom WAF rule.
The source field tells you which Cloudflare feature triggered the action -- waf for managed rules, ipAccess for IP access rules, rateLimit for Rate Limiting rules, and bots for bot management.
Pagination and Large Result Sets
When querying large datasets, use GraphQL pagination with cursors.
{
viewer {
zones(filter: {zoneTag: "ZONE_ID"}) {
httpRequestsAdaptiveGroups(
limit: 1000
filter: {datetime_gt: "2026-06-01T00:00:00Z", datetime_lt: "2026-06-23T00:00:00Z"}
after: "YOUR_CURSOR"
) {
pageInfo {
hasNextPage
endCursor
}
dimensions {
datetime
clientRequestPath
}
sum {
requests
bytes
}
}
}
}
}
Expected output: Each page returns pageInfo.hasNextPage (boolean) and pageInfo.endCursor (string). Set after to the cursor value to fetch the next page. Loop until hasNextPage is false. Each page can return up to 1000 records depending on the dataset and time range.
Common Errors
| Error | Cause | Fix |
|---|---|---|
access_denied |
API token lacks Analytics:read permission | Create a new token with the correct permission scope |
Error: field not found |
Misspelled dataset or field name | Query __schema to list available fields |
Invalid filter |
Filter field does not match the dataset schema | Check that datetime_gt format is ISO 8601 |
Query complexity exceeds limit |
Too many fields or too wide a time range | Reduce the time window or select fewer fields |
401 Unauthorized |
Missing or expired API token | Rotate your token and update environment variables |
Practice Questions
- What is the difference between
httpRequests1mGroupsandhttpRequestsAdaptiveGroupsdatasets? - Which HTTP header do you use to authenticate with the Cloudflare GraphQL API?
- How do you paginate through more than 1000 results in a single dataset query?
FAQ
Summary
Cloudflare GraphQL Analytics API gives you programmatic access to HTTP request metrics, cache analytics, firewall events, and bot management data. You authenticate with an API token, issue POST requests to the GraphQL endpoint, and receive structured JSON responses. Use time-bucketed datasets for trend analysis and adaptive datasets for granular event inspection. Paginate with cursors for large result sets and combine multiple datasets in a single query for comprehensive dashboards.
This approach powers the analytics behind Doda Browser's privacy metrics and Durga Antivirus Pro's threat intelligence feeds. Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro -- security-first tools for the modern web.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro