The Push Notification Bottleneck
Mobile game retention depends on timely, relevant push notifications. Yet most game studios treat push as an afterthought -- sending batch blasts to every active user at a fixed time each day. The result: high opt-out rates, low CTR, and missed revenue opportunities. CCFish needed a better approach: personalized, automated, and serverless.
This post walks through how CCFish built an automated push notification engine on Cloudflare Workers and D1. The system handles player segmentation, delivery scheduling, A/B testing, and performance analytics -- all without provisioning a single server.
Architecture Overview
The push notification system follows an event-driven architecture on Cloudflare Workers:
- **Event triggers** capture player actions (level-up, purchase, 3-day inactive, referral sent)
- **Segmentation engine** classifies each player into behavioral cohorts using D1 query results
- **Campaign scheduler** determines optimal send time based on each player's historical engagement patterns
- **Delivery pipeline** pushes notifications via Firebase Cloud Messaging with rate-limiting
- **Analytics collector** tracks opens, conversions, and opt-out rates back into D1
The entire pipeline runs as a cron-triggered Worker or on-demand via webhook. No queue servers, no message brokers, no dedicated infrastructure.
Step 1: Player Segmentation via D1 Queries
The first challenge is grouping players into actionable segments. CCFish stores player events in D1 tables:
```sql
-- Find players at risk of churn (no login in 5 days, previously active 20+ sessions)
SELECT p.id, p.fcm_token, p.last_login, p.total_purchases
FROM players p
WHERE p.last_login < datetime('now', '-5 days')
AND p.total_sessions > 20
AND p.push_opt_out = 0
AND p.fcm_token IS NOT NULL;
```
This query runs inside a D1-prepared statement on a cron Worker. Each segment has its own query template with configurable parameters (churn window, session threshold, purchase minimum). The system supports six segments: Churn Risk, High Spender, New User, Power User, Lapsed Purchaser, and Referral Trigger.
Step 2: Optimal Send Time Calculation
Batch blasts fail because players engage at different times. CCFish's system calculates each player's optimal send window from historical session data:
```sql
-- Find the 3-hour window when this player has most sessions
SELECT CAST(strftime('%H', created_at) AS INTEGER) as hour,
COUNT(*) as sessions
FROM events
WHERE player_id = ?
AND type = 'session_start'
GROUP BY hour
ORDER BY sessions DESC
LIMIT 1;
```
The result maps to a delivery window: hour 8-11 for morning players, 18-21 for evening players, and 13-16 for afternoon players. Notifications queue in a KV-backed deduplication store and release on schedule.
Step 3: A/B Testing Messaging
Each campaign defines up to 4 message variants. The Worker randomly assigns variants at a configurable ratio. Performance data flows back to D1:
```sql
INSERT INTO push_results (campaign_id, variant, sent, opened, converted)
VALUES (?, ?, 1, ?, ?)
ON CONFLICT(campaign_id, variant) DO UPDATE SET
sent = sent + 1,
opened = opened + ?,
converted = converted + ?;
```
After 1,000 sends per variant, the system automatically shifts 80% traffic to the winning variant. This adaptive optimization runs continuously without manual intervention.
Results
After 8 weeks of automated push campaigns:
- Push notification CTR: 4.2% (vs. 1.8% pre-automation)
- Churn rate reduction: 23% among targeted at-risk players
- Revenue per notification: $0.42 (vs. $0.11 for batch blasts)
- Opt-out rate: 1.2% monthly (industry average: 3-5%)
- Infrastructure cost: $0 (within Workers free tier)
Key Takeaways
- Serverless push automation removes the infrastructure barrier for indie game studios
- D1 analytics enable real-time segmentation without expensive data pipelines
- A/B testing at the notification level continuously improves engagement
- Optimal send time personalization doubles CTR compared to batch blasts
- The entire system runs on Cloudflare's free tier for most game volumes
Implementation Details: The Campaign Builder UI
While the core logic runs on Workers, CCFish also provides a campaign builder interface within the game's admin dashboard. This UI lets marketing managers configure campaigns without touching code:
- **Trigger selection**: Choose from 12 event types (purchase, level-up, referral, inactivity, etc.)
- **Segment filters**: AND/OR conditions on player properties (total spend, sessions, country, device)
- **Message composer**: Rich text editor with emoji support, deep links, and variable insertion
- **Schedule**: Immediate, time-delayed, or recurring (e.g., every Friday at 6 PM)
- **Budget cap**: Max notifications per campaign to prevent over-messaging
The admin panel communicates with the Worker through a REST API secured by API tokens. Campaign configurations are stored in D1's ec_post_meta table, keeping the admin experience responsive even during high-volume push campaigns.
Error Handling and Rate Limiting
Push notification delivery is inherently unreliable -- devices may be offline, FCM tokens may expire, or rate limits may be hit. The CCFish pipeline handles these gracefully:
- **Token rotation**: Inactive FCM tokens are flagged after 3 consecutive delivery failures
- **Rate limiting**: Maximum 200 notifications per minute per device token
- **Retry queue**: Failed deliveries retry up to 3 times with exponential backoff (5min, 15min, 60min)
- **Dead letter**: Notifications that fail all retries are logged to a D1 dead-letter table for manual review
The rate limiter uses Cloudflare Workers' single-flight pattern with KV for counter storage, ensuring accuracy even under concurrent execution.
Measuring Campaign ROI
Every push notification campaign tracks the full attribution funnel:
```sql
SELECT
campaign_id,
COUNT(DISTINCT player_id) as reached,
SUM(CASE WHEN opened = 1 THEN 1 ELSE 0 END) as opens,
SUM(CASE WHEN converted = 1 THEN 1 ELSE 0 END) as conversions,
SUM(CASE WHEN converted = 1 THEN purchase_value ELSE 0 END) as revenue
FROM push_analytics
WHERE sent_at > datetime('now', '-30 days')
GROUP BY campaign_id;
```
This query powers a live dashboard that shows cost-per-conversion, revenue attribution, and projected monthly impact. The entire analytics stack runs on D1 with sub-200ms query times, even when analyzing 100,000+ notification events.