Workers Analytics Engine -- Custom Event Tracking
In this tutorial, you'll learn about Workers Analytics Engine. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Cloudflare Workers Analytics Engine is a high-throughput, SQL-queryable time-series database integrated into the Workers runtime, enabling custom event tracking, usage metering, and real-time analytics without managing external infrastructure.
Why Analytics Engine Matters
Every application needs analytics -- page views, API usage, error rates, user behavior. Traditional approaches send events to external services (Google Analytics, Mixpanel, Datadog), adding latency, cost, and dependency on third parties. Analytics Engine eliminates these by providing built-in event ingestion directly from your Workers, with data stored in Cloudflare's edge network. Events are written with zero additional network hops and are queryable within seconds via a SQL API. Unlike Cloudflare's Logpush which streams raw request logs, Analytics Engine is designed for custom application events with user-defined schemas. This enables product analytics, billing metering, and performance monitoring that would otherwise require a dedicated data pipeline built with REST APIs and a separate database.
Real-world use: A SaaS platform writes an analytics event for every API call, recording endpoint name, response time, user tier, and status code. The platform queries the Analytics Engine dashboard to show real-time usage metrics per customer, driving their billing system.
Analytics Engine Architecture
flowchart LR
W[Worker Code] --> AE[Analytics Engine API]
AE --> B[Batching Layer]
B --> S[(Time-Series Storage)]
S --> SQL[SQL Query API]
SQL --> D[Dashboard]
SQL --> A[Automated Alerts]
subgraph Event_Ingestion
E1[Event 1] --> B
E2[Event 2] --> B
E3[Event 100] --> B
end
style AE fill:#f90,color:#fff
style S fill:#3498db,color:#fff
style SQL fill:#2ecc71,color:#fff
Events are written from Workers using a simple method call. The runtime batches events for efficient ingestion. The SQL query API provides real-time access to the stored data with standard aggregation functions.
Writing Analytics Events
// wrangler.toml
// [[analytics_engine_datasets]]
// binding = "ANALYTICS"
// dataset = "api_events"
export default {
async fetch(request, env) {
const start = Date.now();
const url = new URL(request.url);
const method = request.method;
const response = await fetch('https://api.example.com' + url.pathname, {
method,
headers: request.headers
});
const duration = Date.now() - start;
// Write analytics event
env.ANALYTICS.writeDataPoint({
blobs: [url.pathname, method, String(response.status)],
doubles: [duration, response.headers.get('content-length') || 0],
indexes: [url.pathname]
});
return response;
}
};
// Expected behavior:
// Every request writes one analytics data point
// Events are visible in SQL queries within ~10 seconds
// No external service calls -- zero added latency
The writeDataPoint method accepts three arrays: blobs for string data (up to 20 blobs, 512 bytes each), doubles for numeric data (up to 40 doubles), and indexes for query-filtered strings (up to 2 indexes). The dataset is defined in wrangler.toml with a binding name.
Querying Events with SQL
-- Query: Average response time by endpoint, last hour
SELECT
blob1 AS endpoint,
COUNT(*) AS requests,
AVG(double1) AS avg_duration_ms,
MAX(double1) AS max_duration_ms
FROM api_events
WHERE timestamp >= NOW() - INTERVAL '1' HOUR
AND timestamp < NOW()
GROUP BY blob1
ORDER BY requests DESC
LIMIT 10;
-- Expected output:
-- endpoint | requests | avg_duration_ms | max_duration_ms
-- /api/users | 1850 | 45.2 | 320
-- /api/orders | 920 | 120.7 | 890
-- /api/products | 780 | 32.1 | 150
// Query from another Worker using the SQL API
export default {
async fetch(request, env) {
const query = `
SELECT
blob1 AS endpoint,
COUNT(*) AS total,
AVG(double1) AS avg_latency
FROM api_events
WHERE timestamp >= NOW() - INTERVAL '1' HOUR
GROUP BY blob1
ORDER BY total DESC
`;
const response = await env.ANALYTICS.query(query);
return new Response(JSON.stringify(await response.json()), {
headers: { 'Content-Type': 'application/json' }
});
}
};
// Expected output:
// {"meta": [{"name": "endpoint", "type": "text"}, ...],
// "data": [
// {"endpoint": "/api/users", "total": 1850, "avg_latency": 45.2},
// ...
// ]}
The env.ANALYTICS.query() method executes SQL against your dataset and returns results as a JSON object. The SQL dialect supports standard aggregation, filtering, grouping, and time-range queries. Results typically return in under 100 milliseconds.
Usage Metering for Billing
export default {
async fetch(request, env) {
const apiKey = request.headers.get('X-API-Key');
const { results } = await env.DB.prepare(
'SELECT customer_id, tier FROM customers WHERE api_key = ?'
).bind(apiKey).first();
if (!results) {
return new Response('Unauthorized', { status: 401 });
}
const { customer_id, tier } = results;
const start = Date.now();
const response = await fetch('https://api.example.com/data');
const duration = Date.now() - start;
// Track usage per customer
env.ANALYTICS.writeDataPoint({
blobs: [customer_id, tier, 'data_fetch'],
doubles: [duration, 1],
indexes: [customer_id]
});
return response;
}
};
// Billing query: Count requests per customer this month
const billingQuery = `
SELECT
blob1 AS customer_id,
blob2 AS tier,
SUM(double2) AS request_count
FROM api_events
WHERE timestamp >= DATE_TRUNC('month', NOW())
AND blob1 = 'cust_12345'
GROUP BY blob1, blob2
`;
// Expected output:
// customer_id | tier | request_count
// cust_12345 | pro | 45210
Analytics Engine excels at usage metering because events are written synchronously from the request path with no external dependencies. The SQL API makes it straightforward to generate per-customer usage reports for billing or rate limit enforcement.
Common Errors
| Error | Cause | Fix |
|---|---|---|
Analytics Engine binding not found |
Dataset not defined in wrangler.toml | Add [[analytics_engine_datasets]] with the correct binding and dataset name |
Blob value exceeds maximum length |
String blob longer than 512 bytes | Truncate long strings or split into multiple blobs |
Double array exceeds maximum size |
More than 40 double values provided | Reduce the number of numeric fields per event |
Index array exceeds maximum size |
More than 2 index values provided | Use only the most important filter fields as indexes |
Query timeout |
SQL query scans too much data | Add a time range filter with WHERE timestamp >= NOW() - INTERVAL '1' DAY' |
Practice Questions
- What three types of arrays does
writeDataPointaccept? - How do you define an Analytics Engine dataset in wrangler.toml?
- What SQL function can you use to filter events to a specific time range?
FAQ
{{< faq "Can I query Analytics Engine from outside Cloudflare?">}} Yes. Analytics Engine data can be queried via the Cloudflare API using the account-level SQL query endpoint. This allows you to build external dashboards, export data to BI tools, or integrate with custom reporting pipelines.{{< /faq >}}
Summary
Workers Analytics Engine provides built-in, SQL-queryable event tracking directly from your Workers. Write data points with strings, numbers, and indexes using writeDataPoint(), then query with standard SQL to build dashboards, metering reports, and performance monitoring. Events are ingested with zero added latency and are queryable within seconds. Analytics Engine eliminates the need for external analytics services and Data Pipelines. DodaTech uses Analytics Engine to track API usage across its product suite, powering usage-based billing for DodaZIP and Durga Antivirus Pro.
This guide is brought to you by the developers of Cloudflare, SQL queries, and Durga Antivirus Pro at DodaTech.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro