Cloudflare WebSocket Guide
Cloudflare WebSocket support enables real-time bidirectional communication through Cloudflare’s global network. Whether you’re proxying WebSocket traffic through Cloudflare’s CDN, building serverless WebSocket applications with Workers, or exposing local development servers via Cloudflare Tunnel, understanding how Cloudflare handles WebSocket connections is critical for building reliable real-time applications. This guide covers configuration, implementation patterns, limitations, and how Cloudflare’s WebSocket capabilities compare to other platforms.
Cloudflare WebSocket Proxy Support
Cloudflare proxies WebSocket connections by default on all plans, but the level of support varies. Free and Pro plans support WebSocket proxying without additional configuration, while Business and Enterprise plans receive enhanced WebSocket support with longer connection timeouts and better resource allocation.
When a client initiates a WebSocket handshake to a domain proxied by Cloudflare, the CDN recognizes the Upgrade header and establishes a WebSocket tunnel to your origin server. Cloudflare maintains this connection, applying security rules, DDoS protection, and other configured features while passing WebSocket frames between client and origin.
The WebSocket handshake begins as a standard HTTP request with specific headers. Cloudflare inspects these headers and, upon detecting a valid WebSocket upgrade request, switches protocols and establishes the bidirectional connection. Your origin server must properly handle the upgrade sequence for the connection to succeed.
Standard WebSocket connections work on ports 80 and 443 through Cloudflare’s proxy. Attempting to use non-standard ports requires either bypassing the proxy (setting DNS to DNS-only mode) or using Cloudflare Spectrum, which is available on Enterprise plans for arbitrary TCP/UDP port proxying.
Understanding the fundamentals of WebSocket protocol helps when troubleshooting connection issues through Cloudflare’s proxy layer. The proxy intercepts and inspects the initial handshake but then acts as a transparent tunnel for subsequent WebSocket frames.
Enabling WebSocket Proxy
For domains using Cloudflare’s proxy (orange cloud icon), WebSocket support is enabled automatically. No special configuration is required in the Cloudflare dashboard for basic WebSocket proxying to work.
Verify your DNS records are set to “Proxied” status rather than “DNS only”. The orange cloud icon indicates traffic flows through Cloudflare’s network, allowing the proxy to handle WebSocket upgrade requests. Gray cloud (DNS only) bypasses Cloudflare entirely, sending traffic directly to your origin.
If WebSocket connections fail through the proxy, check your origin server configuration. The server must respond to the WebSocket upgrade request with proper headers, including:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <calculated-value>
Cloudflare’s firewall rules can block WebSocket connections if configured too aggressively. Review your security settings to ensure WebSocket upgrade requests aren’t inadvertently blocked. The connection begins as an HTTP request, so rules that block certain request patterns may prevent the handshake.
For applications requiring longer connection durations, consider upgrading to Business or Enterprise plans. These tiers offer extended timeouts and are better suited for applications with long-lived WebSocket connections that handle infrequent messages.
Cloudflare Workers with WebSocket
Cloudflare Workers support WebSocket connections directly, allowing you to build serverless real-time applications without managing origin servers. A Worker can accept WebSocket connections from clients, process messages, and maintain stateful connections using Durable Objects.
Here’s a basic Cloudflare Worker that accepts WebSocket connections:
export default {
async fetch(request, env) {
const upgradeHeader = request.headers.get('Upgrade');
if (!upgradeHeader || upgradeHeader !== 'websocket') {
return new Response('Expected Upgrade: websocket', { status: 426 });
}
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
server.accept();
server.addEventListener('message', event => {
const message = event.data;
console.log('Received:', message);
server.send(`Echo: ${message}`);
});
server.addEventListener('close', event => {
console.log('WebSocket closed', event.code, event.reason);
});
server.addEventListener('error', event => {
console.log('WebSocket error:', event);
});
return new Response(null, {
status: 101,
webSocket: client,
});
},
};
This Worker creates a WebSocketPair, which provides two connected WebSocket objects. One side is returned to the client, while the Worker retains the other side for processing messages. The accept() method must be called to activate the server-side WebSocket.
For more complex applications, you can proxy WebSocket connections to backend services:
export default {
async fetch(request, env) {
const upgradeHeader = request.headers.get('Upgrade');
if (!upgradeHeader || upgradeHeader !== 'websocket') {
return new Response('Expected WebSocket', { status: 426 });
}
const url = new URL(request.url);
const backendUrl = `wss://backend.example.com${url.pathname}`;
const backendResponse = await fetch(backendUrl, {
headers: request.headers,
});
const webSocket = backendResponse.webSocket;
webSocket.accept();
return new Response(null, {
status: 101,
webSocket: webSocket,
});
},
};
This pattern is useful for adding authentication, rate limiting, or routing logic in front of existing WebSocket servers. The Worker intercepts the connection, performs whatever processing is needed, then establishes the backend connection.
Workers have execution time limits. A Worker handling a WebSocket connection doesn’t run continuously; it only executes during the initial connection and when processing events. Between events, the connection is maintained by Cloudflare’s infrastructure without consuming Worker CPU time.
Durable Objects for Stateful WebSocket Applications
Durable Objects provide strongly consistent, stateful coordination for WebSocket applications. Each Durable Object is a single-threaded instance with dedicated storage, making it ideal for applications where multiple clients need to coordinate through shared state.
A common pattern is using a Durable Object to manage a chat room where multiple WebSocket clients connect and broadcast messages:
export class ChatRoom {
constructor(state, env) {
this.state = state;
this.sessions = [];
}
async fetch(request) {
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();
this.sessions.push(server);
server.addEventListener('message', event => {
const message = event.data;
this.broadcast(message, server);
});
server.addEventListener('close', () => {
this.sessions = this.sessions.filter(s => s !== server);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
broadcast(message, sender) {
for (const session of this.sessions) {
if (session !== sender) {
try {
session.send(message);
} catch (err) {
// Client disconnected, will be cleaned up in close handler
}
}
}
}
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
const roomId = url.pathname.slice(1) || 'default';
const id = env.CHAT_ROOM.idFromName(roomId);
const room = env.CHAT_ROOM.get(id);
return room.fetch(request);
},
};
The Worker entry point extracts a room identifier from the URL path and routes the request to the appropriate Durable Object instance. The Durable Object maintains an array of connected WebSocket sessions and broadcasts messages to all participants except the sender.
Durable Objects also support persistent storage via the state.storage API. You can store message history, user data, or any other state that should survive beyond the current connections:
export class ChatRoom {
constructor(state, env) {
this.state = state;
this.sessions = [];
this.state.blockConcurrencyWhile(async () => {
const stored = await this.state.storage.get('messages');
this.messages = stored || [];
});
}
async fetch(request) {
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();
this.sessions.push(server);
// Send message history to new client
for (const msg of this.messages) {
server.send(msg);
}
server.addEventListener('message', async event => {
const message = event.data;
this.messages.push(message);
if (this.messages.length > 100) {
this.messages.shift();
}
await this.state.storage.put('messages', this.messages);
this.broadcast(message, server);
});
server.addEventListener('close', () => {
this.sessions = this.sessions.filter(s => s !== server);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
broadcast(message, sender) {
for (const session of this.sessions) {
if (session !== sender) {
try {
session.send(message);
} catch (err) {
// Handle error
}
}
}
}
}
This implementation stores the last 100 messages and sends the history to newly connected clients. The storage persists even when no clients are connected, so users joining later can see previous conversation context.
Durable Objects handle WebSocket scalability differently than traditional server clusters. Each Durable Object instance runs in a single location, and all clients for that instance connect to the same data center. This provides strong consistency but requires careful design for globally distributed user bases.
Cloudflare Tunnel for Local WebSocket Servers
Cloudflare Tunnel (formerly Argo Tunnel) exposes local development servers to the internet without opening firewall ports. This is particularly useful for testing WebSocket applications during development or running WebSocket services on infrastructure that doesn’t allow incoming connections.
Install cloudflared:
brew install cloudflare/cloudflare/cloudflared
chmod +x cloudflared
Authenticate cloudflared with your Cloudflare account:
cloudflared tunnel login
Create a tunnel:
cloudflared tunnel create websocket-dev
Configure the tunnel by creating a config.yml file:
tunnel: <tunnel-id>
credentials-file: /path/to/<tunnel-id>.json
ingress:
- hostname: ws.example.com
service: http://localhost:8080
- service: http_status:404
This configuration routes traffic from ws.example.com to a local server on port 8080. The tunnel automatically handles WebSocket upgrade requests without additional configuration.
Start the tunnel:
cloudflared tunnel run websocket-dev
Create a DNS record pointing to the tunnel:
cloudflared tunnel route dns websocket-dev ws.example.com
Your local WebSocket server is now accessible at wss://ws.example.com. Cloudflare Tunnel handles TLS termination automatically, so clients connect via secure WebSocket even if your local server only supports plain HTTP.
For development workflows, you can run ephemeral tunnels without configuration files:
cloudflared tunnel --url http://localhost:8080
This generates a temporary *.trycloudflare.com URL that routes to your local service. The URL changes each time you run the command, making it suitable for quick testing but not production use.
Cloudflare Tunnel supports WebSocket connections with the same timeout and size limits as proxied connections. Long-running connections work correctly, and the tunnel maintains the connection state even if network conditions fluctuate.
WebSocket Timeouts and Limits
Cloudflare imposes timeouts on WebSocket connections that vary by plan tier. Understanding these limits is essential for building reliable applications.
Free and Pro plans have a 100-second timeout for idle WebSocket connections. If no data is transmitted in either direction for 100 seconds, Cloudflare closes the connection. Sending periodic ping frames or application-level heartbeat messages keeps connections alive.
Business and Enterprise plans have a 600-second (10-minute) idle timeout, providing more flexibility for applications with sporadic message patterns.
Implement heartbeat logic to prevent timeout disconnections:
// Client-side heartbeat
const ws = new WebSocket('wss://example.com/ws');
let heartbeatInterval;
ws.onopen = () => {
heartbeatInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // Send every 30 seconds
};
ws.onclose = () => {
clearInterval(heartbeatInterval);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
// Heartbeat acknowledged
return;
}
// Handle other messages
};
On the server side, respond to heartbeat messages to keep the connection active:
server.addEventListener('message', event => {
const data = JSON.parse(event.data);
if (data.type === 'ping') {
server.send(JSON.stringify({ type: 'pong' }));
return;
}
// Handle other messages
});
Cloudflare also enforces a maximum message size. Individual WebSocket frames cannot exceed 100 MB. Most applications use much smaller messages, but if you’re transmitting large data payloads, implement chunking to stay within limits.
Workers have additional constraints. The total size of all WebSocket messages sent or received during a single Worker execution cannot exceed the Worker’s memory limits. For large-scale WebSocket applications, offload message handling to Durable Objects, which have separate resource allocations.
Connection limits depend on your origin server capacity when using proxied mode. When using Workers and Durable Objects, connection limits are governed by Cloudflare’s platform limits rather than origin server constraints. A single Durable Object can handle thousands of concurrent WebSocket connections if designed efficiently.
For proxy configurations similar to those used with traditional servers, review nginx WebSocket proxy patterns to understand how connection limits and timeouts are typically managed in reverse proxy scenarios.
Cloudflare vs Vercel for WebSocket
Vercel does not support persistent WebSocket connections in production deployments. Vercel’s serverless function architecture is designed for short-lived request-response patterns, making it unsuitable for long-lived bidirectional connections.
Cloudflare Workers and Durable Objects natively support WebSocket connections. You can build fully serverless WebSocket applications on Cloudflare without managing any infrastructure. Vercel requires using an external WebSocket server, typically deployed on a different platform like Railway, Render, or a traditional VPS.
The architectural difference stems from execution models. Vercel functions spin up, handle a request, and spin down. WebSocket connections need to remain active for minutes or hours, which doesn’t fit Vercel’s model. Cloudflare’s edge runtime maintains connections between event executions, allowing Workers to handle long-lived connections efficiently.
For applications deployed on Vercel that need real-time functionality, common patterns include:
- Using a separate WebSocket server on another platform
- Implementing Server-Sent Events (SSE) for server-to-client streaming
- Using third-party services like Pusher or Ably for real-time features
Cloudflare provides a complete solution within a single platform. Deploy your HTTP endpoints and WebSocket handlers together in Workers, with Durable Objects managing stateful coordination. This simplifies architecture and reduces operational complexity.
Performance characteristics also differ. Cloudflare runs Workers at edge locations globally, reducing latency for WebSocket handshakes and message transmission. Vercel deploys functions regionally, and adding an external WebSocket server introduces additional network hops and latency.
Cost structures vary significantly. Cloudflare charges based on Worker requests and Durable Object usage. Vercel charges for function execution time. For WebSocket applications with long connection durations but infrequent messages, Cloudflare’s model is typically more cost-effective because you only pay for actual message processing, not for keeping the connection open.
If you’re migrating from Vercel to Cloudflare for WebSocket support, the primary work involves adapting your serverless functions to the Workers API. The WebSocket implementation changes from managing a separate server to using WebSocketPair and Durable Objects.
Frequently Asked Questions
Does Cloudflare support WebSocket on the free plan?
Yes, Cloudflare supports WebSocket proxying on all plans including Free. The main difference between plans is the idle timeout duration. Free and Pro plans have a 100-second timeout, while Business and Enterprise plans support up to 600 seconds. For most applications, implementing heartbeat messages solves timeout concerns on any plan tier.
Can Cloudflare Workers replace a dedicated WebSocket server?
Yes, Cloudflare Workers combined with Durable Objects can fully replace traditional WebSocket servers for many use cases. Workers handle the connection and message routing, while Durable Objects manage stateful coordination between clients. This architecture works well for chat applications, collaborative editing, real-time dashboards, and similar scenarios. Very high-throughput applications with millions of concurrent connections may still benefit from dedicated infrastructure, but Workers scale effectively for most production workloads.
How do I debug WebSocket connections through Cloudflare?
Start by checking the initial handshake using browser developer tools. The Network tab shows the upgrade request and response headers. If the handshake fails, verify your origin server responds with correct headers. For Workers, use console.log() statements, which appear in the Workers dashboard logs. Test connections directly to your origin (bypassing Cloudflare via DNS-only mode) to isolate whether issues stem from your application or Cloudflare configuration. Cloudflare’s dashboard provides analytics for WebSocket connections on Business and Enterprise plans, showing connection counts and error rates.
What happens when a WebSocket connection exceeds the timeout?
When a WebSocket connection remains idle beyond Cloudflare’s timeout limit, Cloudflare closes the connection. The client receives a close event, typically with code 1006 (abnormal closure) since the connection was terminated by the proxy rather than the application. Implement automatic reconnection logic in your client application to handle these closures gracefully. Send periodic messages or use WebSocket ping/pong frames to keep connections active and prevent timeout-related disconnections.