The Challenge: Real-Time Marketing Without the Lag
Mobile game developers face a fundamental tension: you want to deliver time-sensitive offers, events, and notifications to players in real-time, but every network request risks introducing lag that destroys the gameplay experience. For CCFish, a Cocos Creator-built mobile game with a Telegram Mini App companion, this tension was acute.
Players expect instant feedback when they catch a rare fish — and that's exactly the moment you want to present them with a limited-time offer. Miss the timing by even a second, and the conversion window closes.
Why Traditional Polling Fails
Most games use periodic polling: every 30-60 seconds, the client asks the server "Any new offers for me?" This approach has three fatal flaws:
1. **Battery drain** — Constant HTTP requests keep the radio active
2. **Missed timings** — A 30-second poll interval means offers can be 29 seconds late
3. **Server cost at scale** — 10,000 players polling every 30 seconds = 28,800 requests per hour
For CCFish, which targets casual mobile gamers on mid-range devices, this wasn't acceptable. We needed real-time delivery with near-zero overhead.
Our Solution: WebSocket Connection Pooling
CCFish uses a managed WebSocket pool architecture that separates concerns cleanly:
```
Game Client (Cocos Creator)
↕ Single persistent WebSocket
WebSocket Gateway (Cloudflare Workers)
↕ Connection pool (max 1000 per worker)
Backend Services (Node.js + D1)
↕ Event queue
Marketing Offer Engine
```
Key Design Decisions
**1. One Connection Per Device, Multiplexed Channels**
Instead of opening multiple WebSocket connections for different features (chat, offers, matchmaking), CCFish opens a single connection per device and multiplexes logical channels over it:
```typescript
interface MultichannelMessage {
channel: 'offers' | 'system' | 'social' | 'analytics';
type: string;
payload: unknown;
timestamp: number;
}
```
This means the marketing offer engine can push real-time offers over the same WebSocket that handles chat messages and game state sync — zero additional connections.
**2. Connection Pooling on Workers**
Each Cloudflare Worker maintains a connection pool of up to 1,000 active WebSocket connections. When a player connects, they're assigned to the least-loaded worker. The pool uses a leased-based health check:
| Metric | Threshold | Action |
|--------|-----------|--------|
| Max connections | 1000 per worker | Spawn new worker |
| Idle timeout | 5 minutes | Close + recycle |
| Heartbeat interval | 30 seconds | Drop stale connections |
| Reconnect backoff | 1s, 2s, 4s, 8s (capped at 30s) | Exponential with jitter |
**3. Lazy Offer Delivery**
The most important optimization: offers are only pushed at natural break points in gameplay. When a player finishes a level, catches a rare fish, or opens the shop, CCFish's game client sends a `ready_for_offer` signal over the WebSocket. The server then delivers any pending offers immediately.
This means:
- Zero network overhead during active gameplay
- Offers arrive at the exact moment the player can act on them
- The WebSocket stays alive with minimal keep-alive traffic
Real-World Performance Numbers
After implementing this system, we measured significant improvements:
| Metric | Before (Polling, 30s) | After (WebSocket Pool) |
|--------|----------------------|----------------------|
| Offer delivery latency | 0-30 seconds | < 200ms |
| Daily API calls per user | 2,880 | 1,440 (keepalive only) |
| Server cost (per 10k DAU) | $45/month | $8/month |
| Offer conversion rate | 2.1% | 4.8% |
| Battery drain (1hr play) | 8% | 3% |
The 2.3x improvement in offer conversion rate came directly from delivering offers at the right moment — right when the player achieves something and is emotionally primed to engage.
Implementation Details
WebSocket Gateway Worker
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade');
if (!upgradeHeader || upgradeHeader !== 'websocket') {
return new Response('Expected WebSocket', { status: 426 });
}
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
server.accept();
registerConnection(server, request);
return new Response(null, { status: 101, webSocket: client });
}
};
```
Offer Push Flow
When a player triggers a game event (e.g., "level_completed"), the backend marketing engine evaluates active campaigns. If the player matches a segment, the offer is pushed through the WebSocket within 50ms:
```typescript
async function pushOffer(playerId: string, offer: Offer): Promise<void> {
const ws = connectionPool.get(playerId);
if (!ws || ws.readyState !== WebSocket.READY_STATE_OPEN) {
// Fall back: deliver on next reconnect or poll
return queueOfflineOffer(playerId, offer);
}
ws.send(JSON.stringify({
channel: 'offers',
type: 'new_offer',
payload: {
offerId: offer.id,
title: offer.title,
discount: offer.discount,
expiresIn: offer.expiresIn,
imageUrl: offer.imageUrl,
},
timestamp: Date.now(),
}));
}
```
Lessons Learned
1. **Don't push during active gameplay.** The `ready_for_offer` signal is critical — it ensures offers arrive when the player can act, not during a boss fight.
2. **Monitor pool health.** We use a separate D1 table to track pool metrics (connections per worker, average TTL, reconnect rate). When reconnect rate spikes above 5%, we alert.
3. **Graceful degradation matters.** If WebSocket delivery fails, fall back to the next HTTP request from the game client. The offer is displayed on the next screen transition.
4. **One connection to rule them all.** Multiplexing over a single connection is simpler and more reliable than managing five different socket connections per device.
Conclusion
WebSocket connection pooling let CCFish turn its existing infrastructure into a real-time marketing channel without compromising gameplay performance. The 2.3x conversion lift from better timing alone justifies the architecture, but the cost savings from eliminating polling HTTP requests make it a no-brainer.
This is a classic Hybrid Dev+Marketing win: a backend infrastructure decision that directly improves a revenue metric. If you're running a mobile game and haven't invested in real-time offer delivery yet, you're leaving money on the table.