Skip to content

DeploymentCloudflare Workers

Cloudflare Workers

Deploy Courier on Cloudflare Workers — SSE with 100s idle timeout, heartbeat tuning, and WebSocket via Durable Objects.

3 min read

Overview

Cloudflare Workers run at the edge with specific constraints for long-lived connections:

FeatureSupportLimit
HTTPFullStandard request limits
SSESupported100 second idle timeout
WebSocketVia Durable ObjectsRequires 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

src/index.ts
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 app

Heartbeat 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.

Heartbeat timeline
Timeline: 0s ───── 25s (ping) ───── 50s (ping) ───── 75s (ping) ───── 100s limit
                ↑ alive          ↑ alive          ↑ alive          ↑ reset

Default 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:

ts
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.

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.

src/durable-object.ts
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

wrangler.toml
name = "courier-api"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]

[vars]
ALLOWED_ORIGIN = "https://app.example.com"

Limitations

LimitationImpactWorkaround
100s idle timeoutSSE must keep-aliveBuilt-in heartbeat (25s) handles this
No native WS serverCannot use courierWSHandler directlyUse Durable Objects or separate server
No persistent connections across deploysConnections drop on new deploymentClient auto-reconnects via Last-Event-ID
CPU time limits (10ms free / 30s paid)Long SSE generators may hit limitsKeep event generation lightweight
No node:http moduleExpress/Fastify won't workUse 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
src/index.ts
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 ioredis

ioredis 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.