Analytics
Track image requests, bandwidth usage, geographic distribution, and variant popularity across your CDN. Powers the stats dashboard and helps you understand how your images are being served without compromising user privacy.
Setup
Analytics is enabled by default and requires no configuration. However, you'll want to set up batch aggregation for optimal performance:
# Required for batch aggregation (cron job)
CLOUDFLARE_ACCOUNT_ID='your-account-id'
CLOUDFLARE_API_TOKEN='your-api-token-with-analytics-read'
# Optional: Configure behaviour
ANALYTICS_REALTIME_ENABLED='true' # Write to D1 immediately (default: true)
ANALYTICS_BATCH_ENABLED='true' # Aggregate via cron job (default: true)
ANALYTICS_RETENTION_DAYS='90' # Data retention period (default: 90)The API token needs Analytics:Read permissions to query Analytics Engine during batch aggregation.
Aggregation Modes
Pixflare supports two analytics modes that can run simultaneously:
- Real-time - Writes directly to D1 on every image request for instant dashboard updates
- Batch - Aggregates Analytics Engine data daily via cron job for efficiency
You can run both (recommended), one, or neither. Running both provides immediate feedback while the cron job backfills and validates data.
How It Works
Every CDN image request is tracked with privacy-friendly metrics:
sequenceDiagram
participant User
participant CDN as CDN Route
participant AE as Analytics Engine
participant D1 as D1 Database
participant Cron as Cron Job (2 AM UTC)
User->>CDN: GET /user/album/file
CDN->>CDN: Serve image from R2
par Real-time Tracking
CDN->>D1: Write to analytics_daily<br/>owner, date, imageId, variant,<br/>country, requests, bandwidth
and Analytics Engine
CDN->>AE: Write data point<br/>same data + request_id
end
CDN-->>User: Return image
Note over AE: Raw time-series data<br/>retained by Cloudflare
Note over Cron: Daily at 2 AM UTC
Cron->>AE: Query via GraphQL API<br/>yesterday's data
AE-->>Cron: Aggregated rows
Cron->>D1: Insert/update analytics_daily<br/>+ analytics_monthly
Cron->>D1: Update analytics_meta<br/>last_aggregation_dateData Collected
For each image request, the following is tracked:
- Owner - User who owns the image (hashed identifier)
- Image ID - Specific image being served
- Variant - Which size/format (original, w1024, thumb, etc.)
- Status Code - HTTP response code (200, 404, 403, etc.)
- Country - ISO country code from
CF-IPCountryheader - Bandwidth - Bytes transferred (response size)
- Request Count - Always 1 (for aggregation)
No personally identifiable information is collected. No IP addresses, user agents, or referers are stored.
Storage Schema
Data is stored in three D1 tables:
analytics_daily - Per-day metrics grouped by owner, image, variant, and country
(owner, date, image_id, variant, country, requests, bandwidth_bytes)analytics_monthly - Pre-computed monthly totals per user
(owner, year_month, requests, bandwidth_bytes)analytics_meta - Job tracking metadata
(key, value, updated_at)
-- Example: ('last_aggregation_date', '2025-01-05', '2025-01-06T02:00:00Z')Batch Aggregation Process
The cron job runs daily at 2 AM UTC:
- Queries Analytics Engine via Cloudflare's GraphQL API for yesterday's data
- Aggregates by owner + date + image_id + variant + country
- Inserts into
analytics_dailytable (upserts to handle duplicates) - Updates
analytics_monthlywith monthly totals - Records aggregation metadata for monitoring
This approach provides long-term storage beyond Analytics Engine's retention period and enables efficient dashboard queries.
API Endpoints
Query analytics via the REST API:
User Overview
GET /v1/analytics/overview?days=30Returns total requests, bandwidth, top images, and geographic distribution.
Per-Image Stats
GET /v1/analytics/images/:imageId?days=30Returns requests and bandwidth broken down by variant, country, and time series.
Query Parameters
days- Number of days to query (default: 30, max: 90)start- Start date (YYYY-MM-DD)end- End date (YYYY-MM-DD)
Advanced Configuration
Custom Cron Schedule
Change when aggregation runs:
ANALYTICS_AGGREGATION_CRON='0 2 * * *' # Default: 2 AM UTC dailyUses standard cron syntax. Running more frequently doesn't help much since data is batched by day.
Retention Period
Control how long analytics data is kept:
ANALYTICS_RETENTION_DAYS='90' # Default: 90 daysOlder data is automatically purged during the cleanup cron job (runs at 1 AM UTC).
Disable Analytics
To disable analytics entirely:
# Don't set ANALYTICS binding in wrangler.toml
# Or disable both modes:
ANALYTICS_REALTIME_ENABLED='false'
ANALYTICS_BATCH_ENABLED='false'CDN requests will still work but no stats will be collected.
Performance Considerations
Real-time vs Batch
- Real-time - Higher D1 write load (one write per request) but instant dashboard updates
- Batch - Lower D1 load (one aggregation job per day) but dashboard lags by up to 24 hours
- Both - Combines instant feedback with efficient aggregation (recommended)
Cost Implications
Analytics Engine is free for the first 10 million events/month, then $0.05 per million events.
D1 database writes:
- Real-time: 2 writes per image request (per-image + user totals)
- Batch: ~100-1000 writes per day (depending on traffic)
For most use cases, real-time is negligible. Batch mode is always recommended as it's dirt cheap and provides a reliable data source.
Limitations
- Maximum query range: 90 days
- Analytics Engine data is write-only from Workers (can't query directly)
- Batch aggregation requires Cloudflare API credentials
- Country detection relies on Cloudflare's IP geolocation (generally accurate but not perfect)
- Data is aggregated daily, not hourly (sufficient for most use cases)
Troubleshooting
Dashboard shows no data
- Check Analytics Engine binding exists in
wrangler.toml - Verify
ANALYTICS_REALTIME_ENABLEDisn't set tofalse - Confirm images are being served (check CDN routes)
- Wait 24 hours for batch aggregation if only using batch mode
Batch aggregation fails
- Verify
CLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_API_TOKENare set correctly - Check API token has Analytics:Read permission
- Review Worker logs for GraphQL errors
- Ensure Analytics Engine dataset name matches (default:
pixflare_cdn_metrics)