Overview
Cloudflare Workers run at the edge with specific constraints for long-lived connections:
| Feature | Support | Limit |
|---|---|---|
| HTTP | Full | Standard request limits |
| SSE | Supported | 100 second idle timeout |
| WebSocket | Via Durable Objects | Requires paid plan |
SSE on Workers
Courier SSE works on Workers because it uses the Web Standard fetch + ReadableStream API — no Node.js dependencies.
Hono on Workers
import { Hono } from "hono"
import { registerSources } from "@noverax/courier"
import { courierHandler, courierSSEHandler } from "@noverax/courier-hono"
import { sources } from "./api/sources"
registerSources(sources)
const app = new Hono()
app.all("/api/request", courierHandler())
app.get("/api/sse", courierSSEHandler({
sseMaxLifetimeMs: 90_000, // close before 100s idle timeout
sseHeaders: {
"Access-Control-Allow-Origin": "https://app.example.com",
},
}))
app.get("/health", (c) => c.json({ status: "ok" }))
export default appHeartbeat Configuration
Cloudflare terminates connections after 100 seconds of inactivity. The Courier SSE server sends heartbeat pings every 25 seconds by default, which keeps the connection alive.
Timeline: 0s ───── 25s (ping) ───── 50s (ping) ───── 75s (ping) ───── 100s limit
↑ alive ↑ alive ↑ alive ↑ resetDefault heartbeat is sufficient
The built-in 25-second heartbeat interval is well within Cloudflare's 100-second timeout. No additional configuration needed.
Max Lifetime
Even with heartbeats, set sseMaxLifetimeMs to force periodic reconnection. This prevents zombie connections and ensures clean resource rotation:
courierSSEHandler({
sseMaxLifetimeMs: 300_000, // reconnect every 5 minutes
})The client automatically reconnects with Last-Event-ID — no events are lost.
WebSocket on Workers
Standard Cloudflare Workers do not support WebSocket server-side handling (only client-side WebSocket connections). For WebSocket support, you need Durable Objects.
Option A: Separate WS Server (Recommended)
Run WebSocket on a separate Node.js server and keep Workers for HTTP + SSE:
Workers (Edge): HTTP proxy + SSE streaming
Node.js Server: WebSocket (via @noverax/courier-server)This gives you the best of both — edge latency for SSE, full WS support on a dedicated server.
Option B: Durable Objects
Advanced setup
Durable Objects require a Cloudflare paid plan and a custom WebSocket handler. This is not a standard Courier adapter — you need to bridge the Durable Object WebSocket API to Courier manually.
import { DurableObject } from "cloudflare:workers"
export class CourierWSObject extends DurableObject {
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get("Upgrade")
if (upgradeHeader !== "websocket") {
return new Response("Expected WebSocket", { status: 426 })
}
const pair = new WebSocketPair()
const [client, server] = Object.values(pair)
this.ctx.acceptWebSocket(server)
return new Response(null, { status: 101, webSocket: client })
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
// Bridge to Courier WS protocol here
}
async webSocketClose(ws: WebSocket, code: number): Promise<void> {
// Cleanup
}
}Wrangler Configuration
name = "courier-api"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
[vars]
ALLOWED_ORIGIN = "https://app.example.com"Limitations
| Limitation | Impact | Workaround |
|---|---|---|
| 100s idle timeout | SSE must keep-alive | Built-in heartbeat (25s) handles this |
| No native WS server | Cannot use courierWSHandler directly | Use Durable Objects or separate server |
| No persistent connections across deploys | Connections drop on new deployment | Client auto-reconnects via Last-Event-ID |
| CPU time limits (10ms free / 30s paid) | Long SSE generators may hit limits | Keep event generation lightweight |
No node:http module | Express/Fastify won't work | Use Hono (Web Standard APIs) |
Multi-Region
Cloudflare Workers run in 300+ data centers automatically. No replica configuration needed — Workers scale horizontally by design.
For shared state across regions, use:
- Cloudflare KV — eventually consistent key-value store
- Durable Objects — strongly consistent, single-region per object
- External Redis (Upstash) — serverless Redis with global replication
import { createRedisPubSub, createRedisStream } from "@noverax/courier-redis"
import { Redis } from "@upstash/redis"
// Upstash Redis works in Workers (HTTP-based, not TCP)
// Note: requires @upstash/redis, not ioredisioredis compatibility
ioredis uses TCP connections and does not work in Cloudflare Workers. Use Upstash Redis with its HTTP-based client, or connect to Redis from a separate Node.js server.