Cloudflare WebSockets: CDN, Workers & Durable Objects
Cloudflare provides multiple ways to handle WebSocket connections: through their CDN proxy service, Workers for edge computing, and Durable Objects for stateful WebSocket applications. This guide covers all three approaches with production configurations.
Quick Start: CDN Proxy Setup
Section titled âQuick Start: CDN Proxy SetupâCloudflareâs CDN automatically supports WebSocket connections when proxied (orange cloud enabled):
// DNS Configuration (via Cloudflare Dashboard or API){ "type": "A", "name": "ws.example.com", "content": "192.0.2.1", // Your origin server IP "proxied": true, // Orange cloud - enables CDN + WebSocket "ttl": 1 // Auto (when proxied)}CDN Proxy Configuration
Section titled âCDN Proxy ConfigurationâWebSocket Support Requirements
Section titled âWebSocket Support RequirementsâCloudflare CDN proxy supports WebSocket with these characteristics:
- Automatic detection: Recognizes
Upgrade: websocketheaders - Timeout limit: 100 seconds of inactivity (non-configurable)
- No message inspection: Passes frames transparently
- Global anycast network: Routes to nearest data center
- DDoS protection: Automatic mitigation included
Page Rules for WebSocket
Section titled âPage Rules for WebSocketâConfigure specific behaviors for WebSocket endpoints:
// Via Cloudflare APIconst pageRule = { targets: [ { target: 'url', constraint: { operator: 'matches', value: 'ws.example.com/socket/*', }, }, ], actions: [ { id: 'cache_level', value: 'bypass' }, { id: 'disable_apps', value: 'on' }, { id: 'disable_performance', value: 'on' }, { id: 'ssl', value: 'full' }, ], priority: 1, status: 'active',};SSL/TLS Configuration
Section titled âSSL/TLS ConfigurationâConfigure SSL mode for WebSocket connections:
# Recommended SSL settingsSSL/TLS Mode: Full (strict) # Validates origin certificateEdge Certificates: Enabled # Free Universal SSLAlways Use HTTPS: Enabled # Force WSS connectionsMinimum TLS Version: 1.2 # Security baselineFirewall Rules
Section titled âFirewall RulesâCreate firewall rules for WebSocket protection:
// Firewall rule expression(http.request.uri.path contains "/ws" and http.request.headers["upgrade"][0] eq "websocket")and not ip.src in {192.0.2.0/24} // Whitelist IPsand ( cf.threat_score > 30 or // Block high threat scores not cf.bot_management.verified // Block unverified bots)// Action: BlockRate Limiting Rules
Section titled âRate Limiting RulesâProtect WebSocket endpoints from abuse:
// Rate limiting configuration{ "description": "WebSocket connection rate limit", "match": { "request": { "url_pattern": "*/ws*", "methods": ["GET"], "headers": { "Upgrade": "websocket" } } }, "threshold": 10, // 10 requests "period": 60, // per minute "action": { "mode": "challenge", // or "block", "log" "timeout": 3600 // 1 hour }}Cloudflare Workers WebSocket
Section titled âCloudflare Workers WebSocketâBasic Worker WebSocket Handler
Section titled âBasic Worker WebSocket Handlerâexport default { async fetch(request, env, ctx) { const upgradeHeader = request.headers.get('Upgrade');
if (!upgradeHeader || upgradeHeader !== 'websocket') { return new Response('Expected WebSocket', { status: 426 }); }
// Create WebSocket pair const [client, server] = Object.values(new WebSocketPair());
// Handle the WebSocket on the server side server.accept();
server.addEventListener('message', (event) => { // Echo messages back server.send(`Echo: ${event.data}`); });
server.addEventListener('close', () => { console.log('WebSocket closed'); });
// Return WebSocket response return new Response(null, { status: 101, webSocket: client, }); },};Worker with Origin Passthrough
Section titled âWorker with Origin PassthroughâConnect to backend WebSocket server:
export default { async fetch(request, env, ctx) { const url = new URL(request.url);
// Check if it's a WebSocket request if (request.headers.get('Upgrade') === 'websocket') { // Change to backend WebSocket URL url.hostname = 'backend.example.com'; url.protocol = 'wss:';
// Create a new request with modified URL const modifiedRequest = new Request(url, request);
// Add custom headers modifiedRequest.headers.set( 'X-Forwarded-For', request.headers.get('CF-Connecting-IP') ); modifiedRequest.headers.set( 'X-Real-IP', request.headers.get('CF-Connecting-IP') );
// Pass through to origin return fetch(modifiedRequest); }
// Handle non-WebSocket requests return new Response('WebSocket endpoint only', { status: 400 }); },};Worker with Authentication
Section titled âWorker with AuthenticationâImplement JWT authentication:
import jwt from '@tsndr/cloudflare-worker-jwt';
export default { async fetch(request, env, ctx) { // Extract token from query string or header const url = new URL(request.url); const token = url.searchParams.get('token') || request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) { return new Response('Unauthorized', { status: 401 }); }
try { // Verify JWT token const isValid = await jwt.verify(token, env.JWT_SECRET);
if (!isValid) { return new Response('Invalid token', { status: 401 }); }
// Decode token to get user info const payload = jwt.decode(token);
// Create WebSocket connection const [client, server] = Object.values(new WebSocketPair());
// Attach user context const wsHandler = new WebSocketHandler(server, payload.userId);
// Return authenticated WebSocket return new Response(null, { status: 101, webSocket: client, }); } catch (error) { return new Response('Authentication failed', { status: 401 }); } },};
class WebSocketHandler { constructor(websocket, userId) { this.websocket = websocket; this.userId = userId; this.websocket.accept();
this.websocket.addEventListener('message', (event) => { this.handleMessage(event.data); }); }
handleMessage(message) { // Process authenticated messages console.log(`User ${this.userId}: ${message}`); this.websocket.send(`Authenticated echo: ${message}`); }}Durable Objects for Stateful WebSocket
Section titled âDurable Objects for Stateful WebSocketâRoom-Based Chat Implementation
Section titled âRoom-Based Chat Implementationâexport class ChatRoom { constructor(state, env) { this.state = state; this.env = env; this.sessions = []; this.messages = []; }
async fetch(request) { const url = new URL(request.url);
if (request.headers.get('Upgrade') === 'websocket') { // Create WebSocket pair const [client, server] = Object.values(new WebSocketPair());
// Accept WebSocket server.accept();
// Create session const session = { websocket: server, id: crypto.randomUUID(), joined: new Date().toISOString(), ip: request.headers.get('CF-Connecting-IP'), };
// Add to sessions this.sessions.push(session);
// Set up event handlers server.addEventListener('message', async (event) => { await this.handleMessage(session, event.data); });
server.addEventListener('close', () => { this.handleClose(session); });
// Send existing messages for (const msg of this.messages.slice(-50)) { server.send(JSON.stringify(msg)); }
// Broadcast join message this.broadcast( { type: 'user_joined', userId: session.id, timestamp: session.joined, }, session );
return new Response(null, { status: 101, webSocket: client }); }
// Return room statistics return new Response( JSON.stringify({ sessions: this.sessions.length, messages: this.messages.length, }), { headers: { 'Content-Type': 'application/json' }, } ); }
async handleMessage(session, message) { try { const data = JSON.parse(message);
// Store message const msg = { id: crypto.randomUUID(), userId: session.id, content: data.content, timestamp: new Date().toISOString(), };
this.messages.push(msg);
// Persist to storage (optional) await this.state.storage.put(`message:${msg.id}`, msg);
// Broadcast to all sessions this.broadcast(msg); } catch (error) { session.websocket.send( JSON.stringify({ type: 'error', message: 'Invalid message format', }) ); } }
handleClose(session) { // Remove from sessions this.sessions = this.sessions.filter((s) => s.id !== session.id);
// Broadcast leave message this.broadcast({ type: 'user_left', userId: session.id, timestamp: new Date().toISOString(), }); }
broadcast(message, exclude = null) { const data = JSON.stringify(message);
for (const session of this.sessions) { if (session !== exclude) { try { session.websocket.send(data); } catch (error) { // Handle send errors console.error(`Failed to send to ${session.id}:`, error); } } } }}
// worker.js - Routes requests to Durable Objectsexport default { async fetch(request, env, ctx) { const url = new URL(request.url); const roomName = url.pathname.slice(1) || 'default';
// Get Durable Object instance const roomId = env.CHAT_ROOMS.idFromName(roomName); const room = env.CHAT_ROOMS.get(roomId);
// Forward request to Durable Object return room.fetch(request); },};Durable Object with Hibernation
Section titled âDurable Object with HibernationâOptimize costs with WebSocket hibernation:
export class HibernatingChatRoom { constructor(state, env) { this.state = state; this.env = env;
// Enable WebSocket hibernation this.state.getWebSockets().forEach((ws) => { this.handleWebSocket(ws); }); }
async fetch(request) { if (request.headers.get('Upgrade') === 'websocket') { const [client, server] = Object.values(new WebSocketPair());
// Accept and tag the WebSocket this.state.acceptWebSocket(server, { userId: crypto.randomUUID(), joined: Date.now(), });
return new Response(null, { status: 101, webSocket: client }); }
return new Response('WebSocket only', { status: 400 }); }
async webSocketMessage(ws, message) { // Called when hibernated WebSocket receives message const metadata = ws.deserializeAttachment();
// Process message const data = JSON.parse(message); const response = { type: 'message', userId: metadata.userId, content: data.content, timestamp: Date.now(), };
// Broadcast to all connected WebSockets this.state.getWebSockets().forEach((websocket) => { websocket.send(JSON.stringify(response)); }); }
async webSocketClose(ws, code, reason, wasClean) { // Called when hibernated WebSocket closes const metadata = ws.deserializeAttachment();
// Notify others this.state.getWebSockets().forEach((websocket) => { if (websocket !== ws) { websocket.send( JSON.stringify({ type: 'user_left', userId: metadata.userId, }) ); } }); }
handleWebSocket(ws) { // Set up handlers for existing WebSockets after wakeup const metadata = ws.deserializeAttachment(); console.log(`Restored WebSocket for user ${metadata.userId}`); }}State Management and Persistence
Section titled âState Management and Persistenceâexport class PersistentRoom { constructor(state, env) { this.state = state; this.env = env;
// Initialize from storage this.state.blockConcurrencyWhile(async () => { const stored = await this.state.storage.get('roomData'); this.roomData = stored || { messages: [], users: {}, created: Date.now(), }; }); }
async saveState() { // Persist state to storage await this.state.storage.put('roomData', this.roomData); }
async fetch(request) { // Set up alarm for periodic saves const currentAlarm = await this.state.storage.getAlarm(); if (!currentAlarm) { await this.state.storage.setAlarm(Date.now() + 60000); // Every minute }
// Handle WebSocket connections... }
async alarm() { // Called by alarm trigger await this.saveState();
// Clean up old messages const oneHourAgo = Date.now() - 3600000; this.roomData.messages = this.roomData.messages.filter( (msg) => msg.timestamp > oneHourAgo );
// Set next alarm await this.state.storage.setAlarm(Date.now() + 60000); }}Performance Optimization
Section titled âPerformance OptimizationâMessage Batching
Section titled âMessage BatchingâReduce operations by batching messages:
class BatchingWebSocket { constructor(ws) { this.ws = ws; this.queue = []; this.batchTimer = null; this.batchSize = 10; this.batchDelay = 100; // ms }
send(message) { this.queue.push(message);
if (this.queue.length >= this.batchSize) { this.flush(); } else if (!this.batchTimer) { this.batchTimer = setTimeout(() => this.flush(), this.batchDelay); } }
flush() { if (this.queue.length === 0) return;
const batch = { type: 'batch', messages: this.queue.splice(0, this.batchSize), };
this.ws.send(JSON.stringify(batch));
if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; }
// Continue if more messages if (this.queue.length > 0) { this.batchTimer = setTimeout(() => this.flush(), this.batchDelay); } }}Connection Pooling
Section titled âConnection PoolingâManage connection limits efficiently:
class ConnectionPool { constructor(maxConnections = 100) { this.connections = new Map(); this.maxConnections = maxConnections; this.waitQueue = []; }
async addConnection(ws, metadata) { // Check capacity if (this.connections.size >= this.maxConnections) { // Queue or reject if (this.waitQueue.length < 10) { return new Promise((resolve, reject) => { this.waitQueue.push({ ws, metadata, resolve, reject });
// Timeout after 5 seconds setTimeout(() => { const index = this.waitQueue.findIndex((item) => item.ws === ws); if (index !== -1) { this.waitQueue.splice(index, 1); reject(new Error('Connection timeout')); } }, 5000); }); } else { throw new Error('Connection pool full'); } }
// Add connection this.connections.set(ws, { ...metadata, connected: Date.now(), });
return true; }
removeConnection(ws) { this.connections.delete(ws);
// Process wait queue if (this.waitQueue.length > 0) { const { ws: queuedWs, metadata, resolve } = this.waitQueue.shift(); this.addConnection(queuedWs, metadata).then(resolve); } }}Limitations and Workarounds
Section titled âLimitations and WorkaroundsâCloudflare CDN Limitations
Section titled âCloudflare CDN Limitationsâ| Limitation | Value | Workaround |
|---|---|---|
| Idle timeout | 100 seconds | Implement heartbeat/ping |
| Message size | No limit (but counted for billing) | Chunk large messages |
| Connections per IP | No hard limit | Rate limiting recommended |
| Geographic restrictions | Available globally | Use Workers for edge logic |
Worker Limitations
Section titled âWorker Limitationsâ| Resource | Limit | Workaround |
|---|---|---|
| CPU time | 10-50ms | Use Durable Objects for longer processing |
| Memory | 128MB | Stream large data |
| WebSocket connections | 1000 per Worker | Use multiple Workers |
| Subrequests | 50 per request | Batch operations |
Durable Object Limitations
Section titled âDurable Object Limitationsâ| Resource | Limit | Notes |
|---|---|---|
| Memory | 128MB | Per Durable Object instance |
| CPU time | 30 seconds | Per request |
| WebSocket connections | No hard limit | Memory constrained |
| Storage | 1GB | Per Durable Object |
| Storage operations | 1000/sec | Use batching |
Monitoring and Analytics
Section titled âMonitoring and AnalyticsâWorkers Analytics
Section titled âWorkers Analyticsâ// Track WebSocket metricsexport default { async fetch(request, env, ctx) { const startTime = Date.now();
// Track connection attempt await env.METRICS.increment('websocket.connections.attempted');
try { if (request.headers.get('Upgrade') === 'websocket') { const [client, server] = Object.values(new WebSocketPair());
// Track successful connection await env.METRICS.increment('websocket.connections.established');
server.accept();
server.addEventListener('message', async (event) => { // Track messages await env.METRICS.increment('websocket.messages.received'); await env.METRICS.increment( 'websocket.bytes.received', event.data.length ); });
server.addEventListener('close', async () => { // Track disconnection await env.METRICS.increment('websocket.connections.closed');
// Track connection duration const duration = Date.now() - startTime; await env.METRICS.recordTime( 'websocket.connection.duration', duration ); });
return new Response(null, { status: 101, webSocket: client }); } } catch (error) { await env.METRICS.increment('websocket.connections.failed'); throw error; } },};Custom Analytics Dashboard
Section titled âCustom Analytics Dashboardâ// Analytics endpointexport async function handleAnalytics(request, env) { const stats = await env.METRICS.getStats();
return new Response( JSON.stringify({ connections: { attempted: stats['websocket.connections.attempted'] || 0, established: stats['websocket.connections.established'] || 0, failed: stats['websocket.connections.failed'] || 0, active: stats['websocket.connections.active'] || 0, }, messages: { sent: stats['websocket.messages.sent'] || 0, received: stats['websocket.messages.received'] || 0, errors: stats['websocket.messages.errors'] || 0, }, bandwidth: { sent: stats['websocket.bytes.sent'] || 0, received: stats['websocket.bytes.received'] || 0, }, performance: { avgConnectionDuration: stats['websocket.connection.duration.avg'] || 0, avgMessageLatency: stats['websocket.message.latency.avg'] || 0, }, }), { headers: { 'Content-Type': 'application/json' }, } );}Cost Optimization
Section titled âCost OptimizationâUnderstanding Cloudflare Pricing
Section titled âUnderstanding Cloudflare PricingâWorkers pricing components:
- Requests: $0.50 per million requests
- CPU time: $0.02 per million CPU milliseconds
- Durable Objects: $0.15 per million requests + storage
Cost Calculation Example
Section titled âCost Calculation Exampleâfunction calculateMonthlyCost(metrics) { const pricing = { requests: 0.5 / 1_000_000, cpuMs: 0.02 / 1_000_000, durableObjectRequests: 0.15 / 1_000_000, durableObjectStorage: 0.2, // per GB-month bandwidth: 0.09, // per GB after 10TB };
const costs = { workers: metrics.requests * pricing.requests, cpu: metrics.cpuMs * pricing.cpuMs, durableObjects: metrics.doRequests * pricing.durableObjectRequests, storage: metrics.storageGB * pricing.durableObjectStorage, bandwidth: Math.max(0, metrics.bandwidthGB - 10000) * pricing.bandwidth, };
costs.total = Object.values(costs).reduce((a, b) => a + b, 0);
return costs;}
// Example: Chat application with 10,000 daily active usersconst monthlyCost = calculateMonthlyCost({ requests: 10_000 * 100 * 30, // 100 requests per user per day cpuMs: 10_000 * 1000 * 30, // 1 second CPU per user per day doRequests: 10_000 * 50 * 30, // 50 DO requests per user per day storageGB: 10, // 10GB stored messages bandwidthGB: 100, // 100GB monthly bandwidth});
console.log(`Monthly cost: $${monthlyCost.total.toFixed(2)}`);Optimization Strategies
Section titled âOptimization Strategiesâ- Use hibernation: Reduce Durable Object costs
- Implement caching: Minimize origin requests
- Batch operations: Reduce request count
- Compress messages: Lower bandwidth usage
- Regional routing: Use closest data centers
Security Best Practices
Section titled âSecurity Best PracticesâDDoS Protection
Section titled âDDoS Protectionâ// Rate limiting with Durable Objectsexport class RateLimiter { constructor(state, env) { this.state = state; this.connections = new Map(); }
async fetch(request) { const ip = request.headers.get('CF-Connecting-IP'); const now = Date.now();
// Get connection history const history = this.connections.get(ip) || [];
// Remove old entries const recentHistory = history.filter((time) => now - time < 60000);
// Check rate limit if (recentHistory.length >= 10) { return new Response('Rate limit exceeded', { status: 429 }); }
// Add new connection recentHistory.push(now); this.connections.set(ip, recentHistory);
// Allow connection return new Response('OK', { status: 200 }); }}Origin Validation
Section titled âOrigin Validationâ// Validate WebSocket originexport default { async fetch(request, env) { const origin = request.headers.get('Origin'); const allowedOrigins = ['https://example.com', 'https://app.example.com'];
if (!allowedOrigins.includes(origin)) { return new Response('Forbidden', { status: 403 }); }
// Process WebSocket connection... },};Testing WebSocket Connections
Section titled âTesting WebSocket ConnectionsâTesting Cloudflare Workers
Section titled âTesting Cloudflare Workersâimport { unstable_dev } from 'wrangler';
describe('WebSocket Worker', () => { let worker;
beforeAll(async () => { worker = await unstable_dev('src/worker.js', { experimental: { disableExperimentalWarning: true }, }); });
afterAll(async () => { await worker.stop(); });
it('should upgrade to WebSocket', async () => { const response = await worker.fetch('https://example.com', { headers: { Upgrade: 'websocket', Connection: 'Upgrade', }, });
expect(response.status).toBe(101); expect(response.webSocket).toBeDefined(); });});Load Testing
Section titled âLoad Testingâ# Using wscat for basic testingwscat -c wss://ws.example.com/socket
# Using artillery for load testingnpm install -g artilleryartillery quick --count 100 --num 10 wss://ws.example.com/socketTroubleshooting
Section titled âTroubleshootingâCommon Issues
Section titled âCommon Issuesâ-
100-second timeout disconnections
- Implement heartbeat mechanism
- Send ping every 30-60 seconds
-
Connection refused errors
- Verify DNS proxy status (orange cloud)
- Check firewall rules
- Validate SSL configuration
-
High latency
- Use Argo Smart Routing
- Implement regional failover
- Optimize message size
-
Durable Object limits
- Implement connection pooling
- Use hibernation API
- Shard across multiple objects
Best Practices
Section titled âBest Practicesâ- Always use WSS: Cloudflare enforces HTTPS/WSS for proxied connections
- Implement heartbeat: Prevent 100-second timeout disconnections
- Handle reconnection: Build robust reconnection logic client-side
- Monitor metrics: Track connections, errors, and performance
- Use Durable Objects wisely: Stateful applications benefit most
- Implement rate limiting: Protect against abuse
- Validate origins: Prevent CSRF attacks
- Optimize for edge: Minimize subrequests and CPU usage
- Plan for scale: Design with Cloudflareâs limits in mind
- Test globally: Verify performance across regions
Additional Resources
Section titled âAdditional Resourcesâ- Cloudflare WebSocket Documentation
- Durable Objects Documentation
- Workers Pricing
- Cloudflare Network Map
Does Cloudflare support WebSocket connections?
Section titled âDoes Cloudflare support WebSocket connections?âYes. Cloudflare proxies WebSocket connections on all plans, including Free. Enable the orange cloud (proxy) for your DNS record and WebSocket traffic passes through automatically with DDoS protection. No special configuration is needed for basic proxying.
How do Cloudflare Workers handle WebSockets?
Section titled âHow do Cloudflare Workers handle WebSockets?âWorkers can accept WebSocket upgrade requests at the edge using the WebSocket
API. A Worker calls new WebSocketPair() to create a server/client pair,
processes messages in the fetch handler, and responds with HTTP 101. For
persistent state, pair Workers with Durable Objects.
What are Durable Objects and why use them for WebSockets?
Section titled âWhat are Durable Objects and why use them for WebSockets?âDurable Objects provide single-threaded, stateful JavaScript execution at the edge. Each object has a unique ID, persistent storage, and guaranteed single-instance execution. This makes them ideal for WebSocket use cases like chat rooms or collaborative editing where state must be consistent.
What are the Cloudflare WebSocket connection limits?
Section titled âWhat are the Cloudflare WebSocket connection limits?âFree and Pro plans allow WebSocket connections with a 100-second idle timeout. Business and Enterprise plans have higher limits and configurable timeouts. Cloudflare does not charge per WebSocket connection, only per Worker invocation if you use Workers.
Related Content
Section titled âRelated Contentâ- Nginx WebSocket Proxy Guide - Configure Nginx as a WebSocket reverse proxy
- AWS ALB WebSocket Guide - WebSocket configuration for AWS load balancers
- WebSockets at Scale - Architecture patterns for millions of connections
- WebSocket Security Guide - Authentication, encryption, and common vulnerabilities
- Kubernetes WebSocket Guide - Running WebSocket servers in Kubernetes
This guide is maintained by Matthew OâRiordan, Co-founder & CEO of Ably, the real-time data platform. For corrections or suggestions, please open an issue.