diff --git a/README.md b/README.md
index 36b1980..0f39d68 100644
--- a/README.md
+++ b/README.md
@@ -305,6 +305,22 @@ curl -X DELETE https://your-worker.workers.dev/config
| `cloudflare_zones_total` | 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
diff --git a/src/cloudflare/client.ts b/src/cloudflare/client.ts
index 77234f9..f688336 100644
--- a/src/cloudflare/client.ts
+++ b/src/cloudflare/client.ts
@@ -840,8 +840,21 @@ export class CloudflareMetricsClient {
result = await this.gql.query(HTTPMetricsQueryNoBots, queryVars);
}
+ // Safety net: free tier zones should be filtered upstream, but handle gracefully
+ if (result.error?.message.includes("does not have access to the path")) {
+ this.logger.error(
+ "Zone(s) lack GraphQL analytics access - ensure free tier zones are filtered",
+ { error: result.error.message },
+ );
+ return [];
+ }
+
if (result.error) {
- throw new Error(`GraphQL error: ${result.error.message}`);
+ throw new GraphQLError(
+ `GraphQL error: ${result.error.message}`,
+ result.error.graphQLErrors ?? [],
+ { context: { query: "http-metrics", zone_count: zoneIds.length } },
+ );
}
// Initialize all metric definitions
diff --git a/src/cloudflare/queries.ts b/src/cloudflare/queries.ts
index a8ad138..bb0e4a8 100644
--- a/src/cloudflare/queries.ts
+++ b/src/cloudflare/queries.ts
@@ -102,3 +102,38 @@ export const FREE_TIER_QUERIES = [
* Type for free tier query names.
*/
export type FreeTierQuery = (typeof FREE_TIER_QUERIES)[number];
+
+/**
+ * Zone-level GraphQL queries that require paid tier.
+ * Free tier zones don't have access to adaptive analytics endpoints.
+ */
+export const PAID_TIER_GRAPHQL_QUERIES = [
+ "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",
+] as const;
+
+/**
+ * Type for paid tier GraphQL query names.
+ */
+export type PaidTierGraphQLQuery = (typeof PAID_TIER_GRAPHQL_QUERIES)[number];
+
+/**
+ * Type guard for paid tier GraphQL queries.
+ *
+ * @param query Query name to check.
+ * @returns True if query requires paid tier.
+ */
+export function isPaidTierGraphQLQuery(
+ query: string,
+): query is PaidTierGraphQLQuery {
+ return (PAID_TIER_GRAPHQL_QUERIES as readonly string[]).includes(query);
+}
diff --git a/src/components/LandingPage.tsx b/src/components/LandingPage.tsx
index c5fbb79..64865f0 100644
--- a/src/components/LandingPage.tsx
+++ b/src/components/LandingPage.tsx
@@ -441,6 +441,7 @@ export const LandingPage: FC