Skip to content

Cloudflare Containers

Deploy the Unbroken Protocol Bun server to Cloudflare Containers for real SSE and long-poll with persistent connections. Unlike Durable Objects, containers can maintain long-running connections without duration billing concerns.

When to Use Containers vs Durable Objects

AspectDurable ObjectsContainers
Connection modelNon-blocking (returns immediately)Blocking (server waits)
SSE supportSends once, closesTrue streaming
Long-pollReturns 204 immediatelyWaits up to 30s
Client pollIntervalRequiredNot needed
LatencyDepends on poll intervalReal-time
Cost modelPer-request + durationContainer runtime
Best forLow-cost, many streamsReal-time, fewer streams

Choose Containers when:

  • You need true real-time updates (< 100ms latency)
  • You have fewer, high-activity streams
  • You want standard SSE/long-poll behavior

Choose Durable Objects when:

  • You have many low-activity streams
  • Cost optimization is priority
  • Eventual consistency (1-3s delay) is acceptable

Quick Start

1. Create Dockerfile

The @unbroken-protocol/server package includes a ready-to-use Dockerfile:

# packages/server/Dockerfile
FROM oven/bun:1.1-alpine AS base
WORKDIR /app
# Install dependencies
FROM base AS deps
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile --production
# Build stage
FROM base AS build
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
# Production stage
FROM base AS production
RUN addgroup -g 1001 -S unbroken && \
adduser -S unbroken -u 1001
RUN mkdir -p /app/data && chown -R unbroken:unbroken /app/data
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/src/main.ts ./src/main.ts
COPY package.json ./
USER unbroken
ENV PORT=3000
ENV HOST=0.0.0.0
ENV DATA_DIR=/app/data
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["bun", "run", "src/main.ts"]

2. Build and push image

Terminal window
# Build the image
docker build -t unbroken-server ./packages/server
# Tag for your registry
docker tag unbroken-server registry.example.com/unbroken-server:latest
# Push to registry
docker push registry.example.com/unbroken-server:latest

3. Deploy to Cloudflare Containers

Terminal window
# Install wrangler if needed
npm install -g wrangler
# Login to Cloudflare
wrangler login
# Deploy container
wrangler containers deploy \
--name unbroken-streams \
--image registry.example.com/unbroken-server:latest \
--port 3000

Configuration

Environment Variables

VariableDefaultDescription
PORT3000Server port
HOST0.0.0.0Server host
DATA_DIR./dataPersistent storage directory
LONG_POLL_TIMEOUT30000Long-poll timeout (ms)
IDLE_TIMEOUT120Connection idle timeout (s)

Persistent Storage

Mount a volume for data persistence:

Terminal window
wrangler containers deploy \
--name unbroken-streams \
--image registry.example.com/unbroken-server:latest \
--port 3000 \
--volume data:/app/data

Client Usage

With containers, you get true blocking behavior - no pollInterval needed:

import { stream } from "@unbroken-protocol/client"
// SSE - server keeps connection open
const response = await stream({
url: "https://unbroken-streams.example.com/events",
offset: "-1",
live: "sse",
// No pollInterval needed - server streams in real-time
})
for await (const chunk of response.jsonStream()) {
console.log("Real-time data:", chunk)
}
// Long-poll - server waits for data
const response = await stream({
url: "https://unbroken-streams.example.com/events",
offset: "-1",
live: "long-poll",
// No pollInterval needed - server waits up to 30s
})

React Hook

import { useUnbrokenStream } from "@unbroken-protocol/react"
function RealtimeApp() {
const { data, insert, isLoading } = useUnbrokenStream<Message>({
url: "https://unbroken-streams.example.com/messages",
getKey: (msg) => msg.id,
// No pollInterval - uses SSE with true streaming
})
return <MessageList messages={data} />
}

Health Checks

The server exposes a health endpoint:

Terminal window
curl https://unbroken-streams.example.com/health
# {"status":"ok"}

Configure your container orchestrator to use this for liveness/readiness probes.

Scaling

Horizontal Scaling

For multiple container instances, use a shared storage backend:

Terminal window
# Each container mounts the same volume
wrangler containers deploy \
--name unbroken-streams \
--replicas 3 \
--volume shared-data:/app/data

Load Balancing

Cloudflare automatically load balances between container instances. SSE connections are sticky to ensure clients maintain their connection to the same instance.

Comparison: Full Stack

FeatureDO OnlyContainers OnlyDO + Containers
Real-time SSE
Global edge✅ (via DO)
Low cost at scale
True long-poll
Simple setup

For most use cases, choose one approach:

  • Durable Objects: Cost-optimized, global, eventual consistency
  • Containers: Real-time, persistent connections, standard behavior

Local Development

Run the server locally:

Terminal window
cd packages/server
# Install dependencies
bun install
# Start server
bun run start
# or with custom config
PORT=8080 DATA_DIR=./my-data bun run start

Or with Docker:

Terminal window
docker build -t unbroken-server .
docker run -p 3000:3000 -v $(pwd)/data:/app/data unbroken-server

Next Steps