
Cloudflare To Bun
- 26 installs
- 4 repo stars
- Updated January 26, 2026
- daleseo/bun-skills
Migrates Cloudflare Workers to Bun by mapping edge runtime APIs and replacing KV, R2, D1, and Durable Objects bindings for server deployment.
About
Analyzes a Workers project's wrangler config and bindings, then converts edge runtime APIs to Bun equivalents and adapts edge-to-server deployment. Developers use it when moving Cloudflare Workers off the edge onto Bun.
- Maps Cloudflare bindings (KV, R2, D1, Durable Objects) to replacements
- Covers edge-to-server deployment strategies
Cloudflare To Bun by the numbers
- 26 all-time installs (skills.sh)
- Ranked #3,410 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daleseo/bun-skills --skill cloudflare-to-bunAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 26 |
|---|---|
| repo stars | ★ 4 |
| Last updated | January 26, 2026 |
| Repository | daleseo/bun-skills ↗ |
What it does
Migrates Cloudflare Workers to Bun by mapping edge runtime APIs and replacing KV, R2, D1, and Durable Objects bindings for server deployment.
Files
Cloudflare Workers to Bun Migration
You are assisting with migrating Cloudflare Workers applications to Bun. This involves converting edge runtime APIs, replacing Cloudflare bindings, and adapting from edge to server deployment.
Quick Reference
For detailed patterns, see:
- Runtime APIs: runtime-apis.md - Cloudflare to Bun API mapping
- Bindings Migration: bindings.md - KV, R2, D1, Durable Objects replacements
- Deployment: deployment.md - Edge to server deployment strategies
Migration Workflow
1. Pre-Migration Analysis
Check current setup:
# Check Cloudflare CLI
wrangler --version
# Check Bun installation
bun --version
# Analyze wrangler.toml
cat wrangler.tomlReview worker configuration:
# Check compatibility flags
grep compatibility_flags wrangler.toml
# Check bindings
grep -E "kv_namespaces|r2_buckets|d1_databases|durable_objects" wrangler.toml2. Worker Types Analysis
Determine what type of Worker you're migrating:
- Service Worker: Traditional
addEventListener('fetch')format - Module Worker: Modern
export default { fetch() }format - Scheduled Worker: Cron triggers
- Durable Objects: Stateful objects
- Pages Functions: Next.js-like file-based routing
3. Runtime API Conversion
Basic Fetch Handler
Cloudflare Worker (Module format):
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response('Hello World');
},
};Bun Server:
Bun.serve({
port: 3000,
fetch(request: Request): Response | Promise<Response> {
return new Response('Hello World');
},
});
console.log('Server running on http://localhost:3000');Request/Response Handling
Cloudflare Worker:
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/api/data') {
return Response.json({ data: 'value' });
}
return new Response('Not found', { status: 404 });
},
};Bun (with Hono for routing):
import { Hono } from 'hono';
const app = new Hono();
app.get('/api/data', (c) => {
return c.json({ data: 'value' });
});
app.notFound((c) => {
return c.text('Not found', 404);
});
export default {
port: 3000,
fetch: app.fetch,
};For complete API mapping, see runtime-apis.md.
4. Bindings Migration
Cloudflare Workers use bindings for KV, R2, D1, etc. These need to be replaced:
KV Namespace → Database/Cache
Cloudflare Worker:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const value = await env.MY_KV.get('key');
await env.MY_KV.put('key', 'value', { expirationTtl: 3600 });
return Response.json({ value });
},
};Bun (using Redis or SQLite):
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
Bun.serve({
async fetch(request: Request) {
const value = await redis.get('key');
await redis.setex('key', 3600, 'value');
return Response.json({ value });
},
});R2 Bucket → S3 or File System
Cloudflare Worker:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const object = await env.MY_BUCKET.get('file.txt');
const text = await object?.text();
return new Response(text);
},
};Bun (using S3-compatible storage):
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
Bun.serve({
async fetch(request: Request) {
const command = new GetObjectCommand({
Bucket: 'my-bucket',
Key: 'file.txt',
});
const response = await s3.send(command);
const text = await response.Body?.transformToString();
return new Response(text);
},
});For all bindings replacements, see bindings.md.
5. Environment Variables
Cloudflare Worker (wrangler.toml):
[vars]
API_KEY = "dev-key"
[[env.production.vars]]
API_KEY = "prod-key"Bun (.env files):
# .env.development
API_KEY=dev-key
# .env.production
API_KEY=prod-keyAccess in code:
// Both use the same API
const apiKey = process.env.API_KEY;6. Configuration Migration
wrangler.toml:
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
ENVIRONMENT = "production"
kv_namespaces = [
{ binding = "MY_KV", id = "..." }
]
r2_buckets = [
{ binding = "MY_BUCKET", bucket_name = "my-bucket" }
]package.json (Bun):
{
"name": "my-bun-app",
"type": "module",
"scripts": {
"dev": "bun run --hot src/index.ts",
"start": "bun run src/index.ts",
"build": "bun build src/index.ts --outdir=dist"
},
"dependencies": {
"hono": "^3.0.0",
"ioredis": "^5.0.0"
}
}7. Routing Patterns
Cloudflare Worker (manual routing):
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response('Home');
}
if (url.pathname.startsWith('/api/')) {
return handleApi(request);
}
return new Response('Not found', { status: 404 });
},
};Bun (with Hono framework):
import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c) => c.text('Home'));
const api = new Hono();
api.get('/users', (c) => c.json({ users: [] }));
app.route('/api', api);
export default {
port: 3000,
fetch: app.fetch,
};8. Scheduled Events (Cron)
Cloudflare Worker:
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
await doCleanup();
},
};
// wrangler.toml
[triggers]
crons = ["0 0 * * *"] # Daily at midnightBun (using node-cron):
import cron from 'node-cron';
// Run daily at midnight
cron.schedule('0 0 * * *', async () => {
await doCleanup();
});
// Start server
Bun.serve({
fetch(request: Request) {
return new Response('Server running');
},
});9. Testing Migration
Cloudflare Worker (Miniflare):
import { Miniflare } from 'miniflare';
const mf = new Miniflare({
script: `
export default {
fetch() { return new Response('Hello'); }
}
`,
});
const response = await mf.dispatchFetch('http://localhost/');Bun Test:
import { describe, test, expect } from 'bun:test';
describe('Server', () => {
test('should respond to requests', async () => {
const response = await fetch('http://localhost:3000/');
expect(response.status).toBe(200);
});
});10. Deployment Strategy
Cloudflare Workers:
- Edge deployment (globally distributed)
- No cold starts
- Limited runtime (CPU time limits)
- Specialized bindings (KV, R2, D1)
Bun Server:
- Traditional server deployment
- Self-hosted or cloud (AWS, GCP, Azure)
- No CPU time limits
- Standard databases and storage
- Use Docker for containerization (see bun-deploy skill)
For deployment strategies, see deployment.md.
11. Update package.json
{
"name": "migrated-from-cloudflare",
"type": "module",
"scripts": {
"dev": "bun run --hot src/index.ts",
"start": "NODE_ENV=production bun run src/index.ts",
"test": "bun test",
"build": "bun build src/index.ts --outdir=dist --minify"
},
"dependencies": {
"hono": "^3.11.0",
"ioredis": "^5.3.0",
"@aws-sdk/client-s3": "^3.478.0"
},
"devDependencies": {
"@types/bun": "latest"
}
}12. File Structure Migration
Cloudflare Worker:
cloudflare-worker/
├── src/
│ └── index.ts
├── wrangler.toml
└── package.jsonBun Server:
bun-server/
├── src/
│ ├── index.ts
│ ├── routes/
│ └── services/
├── .env.development
├── .env.production
├── package.json
├── tsconfig.json
└── bunfig.tomlMigration Checklist
- [ ] Bun installed and verified
- [ ] Worker type identified (service/module/durable)
- [ ] Bindings mapped to replacements (KV→Redis, R2→S3, etc.)
- [ ] Environment variables migrated
- [ ] Routing migrated (manual → framework)
- [ ] Scheduled tasks migrated (cron triggers)
- [ ] wrangler.toml converted to package.json
- [ ] TypeScript configuration created
- [ ] Dependencies installed with
bun install - [ ] Tests migrated and passing
- [ ] Local server running
- [ ] Deployment strategy planned
Key Differences
| Feature | Cloudflare Workers | Bun Server |
|---|---|---|
| Runtime | Edge (V8 isolates) | Server (JavaScriptCore) |
| Deployment | Global edge network | Traditional hosting |
| Cold Start | ~0ms | Minimal with Bun |
| Execution Time | Limited (CPU time) | Unlimited |
| Storage | KV, R2, D1, DO | Redis, S3, PostgreSQL, etc. |
| Cost Model | Per-request | Server/container costs |
| Scaling | Automatic | Manual/auto-scaling groups |
| State | Durable Objects | Traditional databases |
Common Patterns
CORS Handling
Same in both:
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type',
};
// Handle OPTIONS preflight
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}JSON Responses
Same in both:
return Response.json({ data: 'value' }, {
headers: { 'Cache-Control': 'max-age=3600' }
});Error Handling
Same in both:
try {
// Your code
} catch (error) {
return new Response('Internal Server Error', { status: 500 });
}Completion
Once migration is complete, provide summary:
- ✅ Migration status (success/partial/issues)
- ✅ Bindings replaced (KV→Redis, R2→S3, etc.)
- ✅ Deployment strategy chosen
- ✅ Performance comparison
- ✅ Links to Bun documentation
Next Steps
Suggest to the user: 1. Set up Redis/database for KV replacement 2. Configure S3-compatible storage for R2 replacement 3. Set up monitoring and logging 4. Plan deployment strategy (Docker, cloud hosting) 5. Use bun-deploy skill for containerization 6. Update CI/CD pipelines 7. Load test the migrated application
Cloudflare Bindings to Bun: Replacement Guide
Complete guide for replacing Cloudflare Workers bindings (KV, R2, D1, Durable Objects) with standard solutions in Bun.
KV Namespace → Key-Value Stores
Cloudflare KV is a globally distributed key-value store. Replace with Redis, Upstash, or similar.
Redis Replacement (Recommended)
Cloudflare KV:
export default {
async fetch(request: Request, env: Env) {
// Get
const value = await env.MY_KV.get('key');
const json = await env.MY_KV.get('key', { type: 'json' });
// Put
await env.MY_KV.put('key', 'value');
await env.MY_KV.put('key', 'value', { expirationTtl: 3600 });
// Delete
await env.MY_KV.delete('key');
// List
const keys = await env.MY_KV.list();
},
};Bun with Redis:
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
Bun.serve({
async fetch(request: Request) {
// Get
const value = await redis.get('key');
const json = JSON.parse(await redis.get('key') || 'null');
// Put
await redis.set('key', 'value');
await redis.setex('key', 3600, 'value'); // With TTL
// Delete
await redis.del('key');
// List (scan pattern)
const keys = await redis.keys('*');
// Or use SCAN for production:
// const stream = redis.scanStream();
},
});Installation:
bun add ioredisUpstash Redis (Edge-Compatible)
For edge-like performance:
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// Same API as ioredis
await redis.set('key', 'value');
const value = await redis.get('key');API Comparison
| KV Operation | Redis Equivalent |
|---|---|
get(key) | redis.get(key) |
get(key, {type: 'json'}) | JSON.parse(await redis.get(key)) |
put(key, value) | redis.set(key, value) |
put(key, value, {expirationTtl: n}) | redis.setex(key, n, value) |
delete(key) | redis.del(key) |
list() | redis.keys('*') or redis.scan() |
R2 Bucket → Object Storage
Cloudflare R2 is S3-compatible object storage. Replace with AWS S3, MinIO, or similar.
AWS S3 Replacement
Cloudflare R2:
export default {
async fetch(request: Request, env: Env) {
// Get object
const object = await env.MY_BUCKET.get('file.txt');
const text = await object?.text();
const json = await object?.json();
// Put object
await env.MY_BUCKET.put('file.txt', 'content', {
httpMetadata: {
contentType: 'text/plain',
},
});
// Delete object
await env.MY_BUCKET.delete('file.txt');
// List objects
const list = await env.MY_BUCKET.list();
},
};Bun with AWS S3:
import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
const s3 = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
const BUCKET = 'my-bucket';
Bun.serve({
async fetch(request: Request) {
// Get object
const getCmd = new GetObjectCommand({
Bucket: BUCKET,
Key: 'file.txt',
});
const response = await s3.send(getCmd);
const text = await response.Body?.transformToString();
const json = JSON.parse(text || '{}');
// Put object
const putCmd = new PutObjectCommand({
Bucket: BUCKET,
Key: 'file.txt',
Body: 'content',
ContentType: 'text/plain',
});
await s3.send(putCmd);
// Delete object
const delCmd = new DeleteObjectCommand({
Bucket: BUCKET,
Key: 'file.txt',
});
await s3.send(delCmd);
// List objects
const listCmd = new ListObjectsV2Command({
Bucket: BUCKET,
});
const list = await s3.send(listCmd);
},
});Installation:
bun add @aws-sdk/client-s3MinIO (Self-Hosted S3-Compatible)
import { S3Client } from '@aws-sdk/client-s3';
const s3 = new S3Client({
endpoint: 'http://localhost:9000',
region: 'us-east-1',
credentials: {
accessKeyId: 'minioadmin',
secretAccessKey: 'minioadmin',
},
forcePathStyle: true, // Required for MinIO
});
// Same API as AWS S3D1 Database → SQL Databases
Cloudflare D1 is SQLite at the edge. Replace with PostgreSQL, MySQL, or SQLite.
PostgreSQL Replacement
Cloudflare D1:
export default {
async fetch(request: Request, env: Env) {
// Query
const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(1)
.first();
// Execute
await env.DB.prepare('INSERT INTO users (name) VALUES (?)')
.bind('Alice')
.run();
// Batch
const batch = await env.DB.batch([
env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Bob'),
env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Charlie'),
]);
},
};Bun with PostgreSQL:
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
Bun.serve({
async fetch(request: Request) {
// Query
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[1]
);
const user = result.rows[0];
// Execute
await pool.query(
'INSERT INTO users (name) VALUES ($1)',
['Alice']
);
// Transaction (for batch)
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('INSERT INTO users (name) VALUES ($1)', ['Bob']);
await client.query('INSERT INTO users (name) VALUES ($1)', ['Charlie']);
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
},
});Installation:
bun add pgPrisma ORM (Recommended for Type Safety)
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
Bun.serve({
async fetch(request: Request) {
// Query
const user = await prisma.user.findUnique({
where: { id: 1 },
});
// Insert
await prisma.user.create({
data: { name: 'Alice' },
});
// Batch
await prisma.$transaction([
prisma.user.create({ data: { name: 'Bob' } }),
prisma.user.create({ data: { name: 'Charlie' } }),
]);
},
});Bun's SQLite (For D1-like Experience)
import { Database } from 'bun:sqlite';
const db = new Database('mydb.sqlite');
Bun.serve({
async fetch(request: Request) {
// Query
const query = db.query('SELECT * FROM users WHERE id = ?');
const user = query.get(1);
// Execute
db.run('INSERT INTO users (name) VALUES (?)', ['Alice']);
// Batch (transaction)
db.transaction(() => {
db.run('INSERT INTO users (name) VALUES (?)', ['Bob']);
db.run('INSERT INTO users (name) VALUES (?)', ['Charlie']);
})();
},
});Durable Objects → Traditional Architecture
Durable Objects provide stateful serverless objects. Replace with traditional architecture patterns.
Session Management (Durable Object → Redis)
Cloudflare Durable Objects:
export class SessionDurableObject {
state: DurableObjectState;
sessions: Map<string, any>;
constructor(state: DurableObjectState) {
this.state = state;
this.sessions = new Map();
}
async fetch(request: Request) {
const sessionId = new URL(request.url).searchParams.get('id');
const session = this.sessions.get(sessionId!);
return Response.json({ session });
}
}Bun with Redis:
import { Redis } from 'ioredis';
const redis = new Redis();
Bun.serve({
async fetch(request: Request) {
const url = new URL(request.url);
const sessionId = url.searchParams.get('id');
const session = await redis.get(`session:${sessionId}`);
return Response.json({
session: session ? JSON.parse(session) : null
});
},
});Realtime Collaboration (Durable Object → WebSocket Server)
Cloudflare Durable Objects:
export class RoomDurableObject {
state: DurableObjectState;
connections: Set<WebSocket>;
async fetch(request: Request) {
const [client, server] = Object.values(new WebSocketPair());
server.accept();
this.connections.add(server);
server.addEventListener('message', (msg) => {
for (const conn of this.connections) {
if (conn !== server) conn.send(msg.data);
}
});
return new Response(null, { status: 101, webSocket: client });
}
}Bun WebSocket Server:
const rooms = new Map<string, Set<any>>();
Bun.serve({
fetch(request, server) {
const url = new URL(request.url);
const roomId = url.searchParams.get('room');
const success = server.upgrade(request, {
data: { roomId },
});
if (success) return undefined;
return new Response('WebSocket upgrade failed', { status: 400 });
},
websocket: {
open(ws) {
const { roomId } = ws.data;
if (!rooms.has(roomId)) rooms.set(roomId, new Set());
rooms.get(roomId)!.add(ws);
},
message(ws, message) {
const { roomId } = ws.data;
const room = rooms.get(roomId);
if (room) {
for (const conn of room) {
if (conn !== ws) conn.send(message);
}
}
},
close(ws) {
const { roomId } = ws.data;
rooms.get(roomId)?.delete(ws);
},
},
});Service Bindings → HTTP Calls
Cloudflare Service Bindings:
export default {
async fetch(request: Request, env: Env) {
const response = await env.MY_SERVICE.fetch(request);
return response;
},
};Bun (standard HTTP):
Bun.serve({
async fetch(request: Request) {
const response = await fetch('http://my-service.internal/api', {
method: request.method,
headers: request.headers,
body: request.body,
});
return response;
},
});Queue → Message Queues
Cloudflare Queues:
export default {
async fetch(request: Request, env: Env) {
await env.MY_QUEUE.send({ id: 123, data: 'value' });
},
async queue(batch: MessageBatch, env: Env) {
for (const message of batch.messages) {
await processMessage(message.body);
}
},
};Bun with BullMQ (Redis-based):
import { Queue, Worker } from 'bullmq';
const queue = new Queue('my-queue', {
connection: { host: 'localhost', port: 6379 },
});
Bun.serve({
async fetch(request: Request) {
await queue.add('job', { id: 123, data: 'value' });
return new Response('Queued');
},
});
// Worker (separate process)
const worker = new Worker('my-queue', async (job) => {
await processMessage(job.data);
}, {
connection: { host: 'localhost', port: 6379 },
});Summary Table
| Cloudflare Binding | Bun Replacement | Package |
|---|---|---|
| KV Namespace | Redis, Upstash | ioredis, @upstash/redis |
| R2 Bucket | S3, MinIO | @aws-sdk/client-s3 |
| D1 Database | PostgreSQL, MySQL, SQLite | pg, mysql2, bun:sqlite, @prisma/client |
| Durable Objects | Redis + Architecture | ioredis + design patterns |
| Service Bindings | HTTP fetch | Built-in fetch |
| Queues | BullMQ, RabbitMQ | bullmq, amqplib |
| Analytics Engine | ClickHouse, TimescaleDB | Database-specific clients |
| Email (MailChannels) | Resend, SendGrid | resend, @sendgrid/mail |
Environment Variables
All binding references should use environment variables:
# .env
REDIS_URL=redis://localhost:6379
DATABASE_URL=postgresql://user:pass@localhost:5432/db
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=us-east-1
S3_BUCKET=my-bucketAccess in code:
const redis = new Redis(process.env.REDIS_URL);This makes testing and deployment across environments easier.
Cloudflare Workers to Bun: Deployment Strategies
Guide for transitioning from edge deployment (Cloudflare Workers) to traditional server deployment (Bun).
Deployment Architecture Comparison
Cloudflare Workers
- Location: Global edge network (~300 locations)
- Execution: V8 isolates (shared infrastructure)
- Scaling: Automatic, per-request
- Cold Start: ~0ms (isolates)
- Pricing: Pay-per-request
- State: Ephemeral (Durable Objects for state)
- Limits: CPU time limits, memory limits
Bun Server
- Location: Self-hosted or cloud regions
- Execution: Dedicated containers/VMs
- Scaling: Manual or auto-scaling groups
- Cold Start: Minimal (~10ms with Bun)
- Pricing: Server/container costs
- State: Can maintain in-memory state
- Limits: Based on server resources
Deployment Options
1. Docker Deployment (Recommended)
Use the bun-deploy skill for complete Docker deployment guide.
Basic Dockerfile:
FROM oven/bun:1-alpine AS deps
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
FROM oven/bun:1-alpine
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]Deploy to:
- Docker Hub / GitHub Container Registry
- AWS ECS / Fargate
- Google Cloud Run
- Azure Container Instances
- DigitalOcean App Platform
- Fly.io
- Railway
2. Platform-as-a-Service (PaaS)
Railway
# Install Railway CLI
npm install -g @railway/cli
# Login and deploy
railway login
railway init
railway uprailway.json:
{
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "bun run src/index.ts",
"restartPolicyType": "ON_FAILURE"
}
}Fly.io
# Install Fly CLI
curl -L https://fly.io/install.sh | sh
# Initialize and deploy
fly launch
fly deployfly.toml:
app = "my-bun-app"
[build]
image = "oven/bun:1"
[[services]]
internal_port = 3000
protocol = "tcp"
[[services.ports]]
port = 80
handlers = ["http"]
[[services.ports]]
port = 443
handlers = ["tls", "http"]3. Cloud Platforms
AWS (ECS/Fargate)
# Build and push Docker image
docker build -t my-bun-app .
docker tag my-bun-app:latest ${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/my-bun-app:latest
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/my-bun-app:latest
# Deploy via ECS
aws ecs update-service --cluster my-cluster --service my-service --force-new-deploymenttask-definition.json:
{
"family": "my-bun-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"containerDefinitions": [
{
"name": "app",
"image": "${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/my-bun-app:latest",
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"environment": [
{
"name": "NODE_ENV",
"value": "production"
}
]
}
]
}Google Cloud Run
# Build and deploy
gcloud builds submit --tag gcr.io/${PROJECT_ID}/my-bun-app
gcloud run deploy my-bun-app \
--image gcr.io/${PROJECT_ID}/my-bun-app \
--platform managed \
--region us-central1 \
--allow-unauthenticatedAzure Container Instances
# Create resource group
az group create --name myResourceGroup --location eastus
# Deploy container
az container create \
--resource-group myResourceGroup \
--name my-bun-app \
--image myregistry.azurecr.io/my-bun-app:latest \
--dns-name-label my-bun-app \
--ports 30004. Kubernetes (For Production at Scale)
See bun-deploy skill for complete Kubernetes manifests.
Basic deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: bun-app
spec:
replicas: 3
selector:
matchLabels:
app: bun-app
template:
metadata:
labels:
app: bun-app
spec:
containers:
- name: app
image: my-bun-app:latest
ports:
- containerPort: 3000
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: bun-app
spec:
selector:
app: bun-app
ports:
- port: 80
targetPort: 3000
type: LoadBalancerCDN and Edge Caching
Since Bun runs on origin servers, add a CDN for edge caching:
Cloudflare (as CDN)
Keep using Cloudflare for caching:
// Set cache headers in Bun
Bun.serve({
fetch(request) {
return new Response('Hello', {
headers: {
'Cache-Control': 'public, max-age=3600',
'CDN-Cache-Control': 'max-age=86400', // Cloudflare respects this
}
});
},
});Fastly / Akamai / AWS CloudFront
Similar cache header strategies work across CDNs:
const headers = {
'Cache-Control': 'public, max-age=3600, s-maxage=86400',
'Surrogate-Control': 'max-age=604800',
'Vary': 'Accept-Encoding',
};Multi-Region Deployment
Global Distribution
Option 1: Deploy to multiple regions
- us-east-1 (AWS Virginia)
- eu-west-1 (AWS Ireland)
- ap-southeast-1 (AWS Singapore)Use GeoDNS (Route 53, Cloudflare) to route users to nearest region.
Option 2: Edge caching with single origin
Cloudflare CDN (global) → Bun server (single region)Most traffic served from edge cache, origin handles cache misses.
Scaling Strategies
Horizontal Scaling
Auto-scaling group (AWS):
# Auto Scaling Group configuration
MinSize: 2
MaxSize: 10
TargetTrackingScaling:
- Type: TargetTrackingScaling
TargetValue: 70.0
PredefinedMetricSpecification:
PredefinedMetricType: ASGAverageCPUUtilizationKubernetes HPA:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: bun-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: bun-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70Vertical Scaling
Increase container resources:
resources:
requests:
memory: "256Mi" # Increased from 128Mi
cpu: "200m" # Increased from 100m
limits:
memory: "1Gi" # Increased from 512Mi
cpu: "1000m" # Increased from 500mMonitoring and Observability
Application Performance Monitoring (APM)
Datadog:
import tracer from 'dd-trace';
tracer.init({
service: 'bun-app',
env: 'production',
});
Bun.serve({
fetch(request) {
const span = tracer.scope().active();
span?.setTag('http.url', request.url);
return new Response('Hello');
},
});New Relic:
import newrelic from 'newrelic';
Bun.serve({
fetch(request) {
return newrelic.startWebTransaction(request.url, async () => {
return new Response('Hello');
});
},
});Logging
Structured logging:
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true
}
}
});
Bun.serve({
fetch(request) {
logger.info({ url: request.url, method: request.method }, 'Request received');
return new Response('Hello');
},
});Log aggregation:
- CloudWatch Logs (AWS)
- Cloud Logging (GCP)
- Azure Monitor
- Datadog Logs
- Elasticsearch/Logstash/Kibana (ELK)
Health Checks and Load Balancing
Health check endpoint:
Bun.serve({
fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/health') {
return Response.json({
status: 'healthy',
uptime: process.uptime(),
timestamp: Date.now(),
});
}
// Your app logic
},
});Load balancer configuration (ALB):
HealthCheck:
Path: /health
Protocol: HTTP
Port: 3000
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
Interval: 30
Timeout: 5CI/CD Pipeline
GitHub Actions
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run tests
run: bun test
- name: Build Docker image
run: docker build -t my-bun-app:${{ github.sha }} .
- name: Push to registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker tag my-bun-app:${{ github.sha }} myregistry/my-bun-app:latest
docker push myregistry/my-bun-app:latest
- name: Deploy to production
run: |
# Deploy command (e.g., kubectl, aws ecs update-service)
kubectl set image deployment/bun-app app=myregistry/my-bun-app:latestCost Comparison
Cloudflare Workers
- Free Tier: 100k requests/day
- Paid: $5/month for 10M requests
- Cost Model: Pay-per-request
Bun on Cloud
AWS Fargate (example):
- Instance: 0.25 vCPU, 0.5GB RAM
- Cost: ~$15/month (running 24/7)
- Traffic: Unlimited requests (within instance capacity)
- Scaling: Additional containers as needed
DigitalOcean (example):
- Basic Droplet: 1 vCPU, 1GB RAM
- Cost: $6/month
- Traffic: 1TB included
Trade-offs:
- Workers: Better for low/variable traffic
- Bun: Better for consistent traffic or high request volumes
Migration Deployment Strategy
Zero-Downtime Migration
Phase 1: Shadow deployment
- Deploy Bun server
- Keep Cloudflare Worker running
- Send duplicate requests to both (via Worker)
- Compare responses
Phase 2: Gradual rollout
- Route 10% traffic to Bun
- Monitor metrics (latency, errors)
- Gradually increase to 100%
Phase 3: Full cutover
- Route 100% traffic to Bun
- Keep Worker as backup for 1 week
- Decommission Worker
DNS Strategy
1. Worker: worker.example.com (all traffic)
2. Add Bun: api.example.com
3. Test Bun thoroughly
4. Update DNS: api.example.com → Bun server
5. Deprecate worker.example.comBest Practices
1. Use CDN: Add Cloudflare/Fastly in front for edge caching 2. Multi-region: Deploy to multiple regions for low latency 3. Auto-scaling: Configure HPA or auto-scaling groups 4. Monitoring: Set up APM and logging from day one 5. Health checks: Implement robust health endpoints 6. Graceful shutdown: Handle SIGTERM properly 7. Container optimization: Use multi-stage Docker builds 8. Security: Use secrets management, not env files 9. CI/CD: Automate testing and deployment 10. Rollback plan: Keep previous versions for quick rollback
Summary
| Aspect | Cloudflare Workers | Bun Deployment |
|---|---|---|
| Complexity | Low | Medium-High |
| Control | Limited | Full |
| Cost (low traffic) | Lower | Higher (minimum server cost) |
| Cost (high traffic) | Higher (per-request) | Lower (fixed server cost) |
| Latency | Lower (global edge) | Higher (regional origin) |
| Cold Start | ~0ms | Minimal (~10ms) |
| State | Limited (DO) | Flexible |
| CPU Limits | 10-50ms | Unlimited |
Choose Bun deployment when you need:
- Full control over infrastructure
- Long-running computations
- Complex state management
- Cost predictability at scale
- Integration with existing services
Cloudflare Workers to Bun: Runtime API Mapping
Complete mapping of Cloudflare Workers runtime APIs to Bun equivalents.
Core APIs (Identical)
These Web Standard APIs work the same in both:
| API | Status | Notes |
|---|---|---|
Request | ✅ Identical | Standard Web API |
Response | ✅ Identical | Standard Web API |
Headers | ✅ Identical | Standard Web API |
URL | ✅ Identical | Standard Web API |
URLSearchParams | ✅ Identical | Standard Web API |
fetch() | ✅ Identical | Standard Web API |
crypto.* | ✅ Identical | Web Crypto API |
TextEncoder | ✅ Identical | Standard Web API |
TextDecoder | ✅ Identical | Standard Web API |
AbortController | ✅ Identical | Standard Web API |
ReadableStream | ✅ Identical | Streams API |
WritableStream | ✅ Identical | Streams API |
Worker Entry Points
Module Worker (Modern)
Cloudflare:
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
return new Response('Hello World');
},
};Bun:
Bun.serve({
port: 3000,
async fetch(request: Request): Promise<Response> {
return new Response('Hello World');
},
});Service Worker (Legacy)
Cloudflare:
addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request: Request): Promise<Response> {
return new Response('Hello World');
}Bun:
Bun.serve({
port: 3000,
async fetch(request: Request): Promise<Response> {
return new Response('Hello World');
},
});ExecutionContext Methods
Cloudflare's ctx parameter doesn't exist in Bun. Replace patterns:
ctx.waitUntil()
Cloudflare:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// Don't wait for this to complete
ctx.waitUntil(
logAnalytics(request)
);
return new Response('OK');
},
};Bun:
Bun.serve({
async fetch(request: Request) {
// Fire and forget
logAnalytics(request).catch(console.error);
return new Response('OK');
},
});ctx.passThroughOnException()
Cloudflare:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
ctx.passThroughOnException();
// If exception, fall through to origin
},
};Bun (no equivalent, handle errors explicitly):
Bun.serve({
async fetch(request: Request) {
try {
// Your code
return new Response('OK');
} catch (error) {
// Forward to origin or handle error
console.error(error);
return new Response('Error', { status: 500 });
}
},
});Cache API
Cloudflare Workers Cache
Cloudflare:
export default {
async fetch(request: Request) {
const cache = caches.default;
let response = await cache.match(request);
if (!response) {
response = await fetch(request);
await cache.put(request, response.clone());
}
return response;
},
};Bun (implement with Redis or in-memory):
import { Redis } from 'ioredis';
const redis = new Redis();
Bun.serve({
async fetch(request: Request) {
const cacheKey = new URL(request.url).pathname;
const cached = await redis.get(cacheKey);
if (cached) {
return new Response(cached, {
headers: { 'X-Cache': 'HIT' }
});
}
const response = await fetch(request);
const data = await response.text();
await redis.setex(cacheKey, 3600, data);
return new Response(data, {
headers: { 'X-Cache': 'MISS' }
});
},
});WebSocket
Cloudflare Workers
Cloudflare:
export default {
async fetch(request: Request) {
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader === 'websocket') {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
server.accept();
server.addEventListener('message', (event) => {
server.send(`Echo: ${event.data}`);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
return new Response('Expected WebSocket', { status: 400 });
},
};Bun:
Bun.serve({
fetch(request, server) {
const success = server.upgrade(request);
if (success) {
return undefined;
}
return new Response('Expected WebSocket', { status: 400 });
},
websocket: {
message(ws, message) {
ws.send(`Echo: ${message}`);
},
open(ws) {
console.log('Client connected');
},
close(ws) {
console.log('Client disconnected');
},
},
});HTMLRewriter
Cloudflare's HTMLRewriter doesn't have a direct Bun equivalent.
Cloudflare:
export default {
async fetch(request: Request) {
const response = await fetch(request);
return new HTMLRewriter()
.on('h1', {
element(element) {
element.setInnerContent('Modified Title');
},
})
.transform(response);
},
};Bun (use cheerio or jsdom):
import * as cheerio from 'cheerio';
Bun.serve({
async fetch(request: Request) {
const response = await fetch(request);
const html = await response.text();
const $ = cheerio.load(html);
$('h1').text('Modified Title');
return new Response($.html(), {
headers: { 'Content-Type': 'text/html' }
});
},
});Cloudflare-Specific APIs (No Direct Equivalent)
Runtime APIs
| Cloudflare API | Bun Alternative | Notes |
|---|---|---|
navigator.userAgent | request.headers.get('user-agent') | Same in both |
caches | Redis, in-memory cache | Not built-in |
HTMLRewriter | cheerio, jsdom | NPM packages |
WebSocketPair | Bun.serve({ websocket }) | Different API |
ScheduledEvent | node-cron, cron jobs | NPM packages |
DurableObjectNamespace | Database + Redis | Different architecture |
Performance APIs
Response Headers for Performance
Same in both:
return new Response('Hello', {
headers: {
'Cache-Control': 'public, max-age=3600',
'CDN-Cache-Control': 'max-age=86400',
}
});Compression
Cloudflare (automatic):
// Cloudflare auto-compresses responses
return new Response(largeData);Bun (manual with Hono middleware):
import { Hono } from 'hono';
import { compress } from 'hono/compress';
const app = new Hono();
app.use('*', compress());
app.get('/', (c) => c.text(largeData));
export default app;Environment Access
Cloudflare (via env parameter):
export default {
async fetch(request: Request, env: Env) {
const apiKey = env.API_KEY;
const db = env.DB;
},
};Bun (via process.env):
Bun.serve({
async fetch(request: Request) {
const apiKey = process.env.API_KEY;
// Database connections initialized separately
},
});Request Context
Getting Client IP
Cloudflare:
const clientIp = request.headers.get('CF-Connecting-IP');Bun:
// Behind proxy (nginx, cloudflare)
const clientIp = request.headers.get('X-Forwarded-For')?.split(',')[0];
// Direct connection
Bun.serve({
fetch(request, server) {
const clientIp = server.requestIP(request);
},
});Getting Request Country/Region
Cloudflare:
const country = request.cf?.country;
const city = request.cf?.city;Bun (use GeoIP library):
import geoip from 'geoip-lite';
const clientIp = request.headers.get('X-Forwarded-For')?.split(',')[0];
const geo = geoip.lookup(clientIp);
const country = geo?.country;
const city = geo?.city;Streaming Responses
Same API in both:
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk 1\n'));
controller.enqueue(new TextEncoder().encode('chunk 2\n'));
controller.close();
}
});
return new Response(stream, {
headers: { 'Content-Type': 'text/plain' }
});FormData Handling
Same in both:
const formData = await request.formData();
const file = formData.get('file') as File;
const name = formData.get('name') as string;JSON Handling
Same in both:
// Parse request JSON
const data = await request.json();
// Return JSON response
return Response.json({ message: 'Success' });Error Handling
Same pattern in both:
try {
const result = await riskyOperation();
return Response.json({ result });
} catch (error) {
console.error(error);
return new Response('Internal Server Error', {
status: 500
});
}Summary
High Compatibility (✅):
- Standard Web APIs (fetch, Request, Response, etc.)
- JSON/FormData handling
- Streaming responses
- WebSocket (different API, same capability)
Needs Replacement (⚠️):
- ExecutionContext methods → Fire-and-forget promises
- Cache API → Redis or in-memory cache
- HTMLRewriter → cheerio/jsdom
- Bindings → Standard databases/services
No Equivalent (❌):
- Edge-specific features (distributed execution)
- CF-specific headers → GeoIP libraries
- Durable Objects → Traditional databases + architecture
Most Worker code can be migrated with minimal changes since both use standard Web APIs.