Cloudflare Workers — Edge Functions (Azure Functions Equivalent)
Cloudflare Workers is the serverless function platform equivalent to Azure Functions, but the execution model is fundamentally different.
| Aspect | Azure Functions | Cloudflare Workers |
|---|---|---|
| Execution location | Specific region (japaneast, etc.) | Nearest edge PoP to the user (300+ locations) |
| Cold start | Yes (Consumption plan: hundreds of ms to seconds) | None (V8 Isolates are pre-warmed) |
| Scale out | Instances are replicated (can be stateful) | Isolate per request (stateless) |
| Max execution time | 10 min (Consumption) to unlimited (Premium/Dedicated) | CPU time: 10ms free / 30s paid |
| Runtime | .NET / Node.js / Python / Java / etc. | JavaScript / TypeScript / Python / Rust |
| Pricing | Execution time × memory | Requests (free: 100K/day) |
Cloudflare Workers does not support C#/.NET. While it is technically possible to compile .NET to WebAssembly, the resulting bundle size (tens of MB) far exceeds the Workers limit (10MB compressed on the paid plan), making this approach impractical.
For .NET migration and co-existence strategies, see the section below.
Trigger Comparison
Workers handlers that correspond to Azure Functions triggers:
| Azure Functions Trigger | Workers Handler | Purpose |
|---|---|---|
| HTTP Trigger | fetch | HTTP request handling |
| Timer Trigger | scheduled | Cron-based recurring execution |
| Queue Trigger (Service Bus) | queue | Consuming from Cloudflare Queues |
| Blob Trigger | (none — R2 events via separate config) | — |
| Event Grid Trigger | (none) | — |
| Email Trigger | email | Cloudflare Email Routing |
Binding Comparison
Workers Bindings correspond to Azure Functions input/output bindings. They are accessed via the env object.
| Azure Functions Binding | Workers Binding | Description |
|---|---|---|
| Blob Storage (in/out) | R2Bucket | Object storage |
| Table Storage | KVNamespace | Key-value store |
| Service Bus | Queue | Message queue |
| SQL Database | D1Database | SQLite edge DB |
| Cosmos DB | KVNamespace / D1Database | Choose based on use case |
| SignalR | DurableObject | WebSocket / real-time |
| HTTP (output) | Fetcher (Service Binding) | Calling another Worker |
Basic Worker Structure
HTTP Trigger
// src/index.ts
export interface Env {
// Declare bindings defined in wrangler.toml as types
MY_KV: KVNamespace;
MY_BUCKET: R2Bucket;
MY_DB: D1Database;
API_KEY: string; // set via: wrangler secret put API_KEY
}
export default {
// Equivalent to Azure Functions' Run(HttpRequest req, ...)
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
// Routing (equivalent to function.json route config)
if (url.pathname === '/api/hello' && request.method === 'GET') {
return Response.json({ message: 'Hello from the edge!' });
}
if (url.pathname === '/api/items' && request.method === 'POST') {
const body = await request.json<{ name: string }>();
// Write to KV (equivalent to Azure Table Storage output binding)
await env.MY_KV.put(`item:${crypto.randomUUID()}`, JSON.stringify(body));
return Response.json({ ok: true }, { status: 201 });
}
return new Response('Not Found', { status: 404 });
},
};
Timer Trigger (Cron)
Equivalent to Azure Functions TimerTrigger. Define the cron schedule in wrangler.toml.
# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-11-01"
[[triggers]]
crons = ["0 */6 * * *"] # every 6 hours
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return new Response('OK');
},
// Equivalent to Azure Functions TimerTrigger
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
console.log(`Cron triggered: ${event.cron} at ${event.scheduledTime}`);
// Example: clean up old records in D1
await env.MY_DB.prepare(
'DELETE FROM logs WHERE created_at < ?'
).bind(Date.now() - 7 * 24 * 60 * 60 * 1000).run();
},
};
Queue Consumer (Queue Trigger)
Equivalent to Azure Functions Service Bus Queue Trigger.
# wrangler.toml
[[queues.consumers]]
queue = "my-queue"
max_batch_size = 10
max_batch_timeout = 30
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return new Response('OK');
},
// Equivalent to Azure Functions ServiceBusTrigger
async queue(batch: MessageBatch<{ orderId: string }>, env: Env): Promise<void> {
for (const message of batch.messages) {
try {
await processOrder(message.body.orderId, env);
message.ack(); // success
} catch (err) {
message.retry(); // return to retry queue
}
}
},
};
Using Resource Bindings
KV (Key-Value Store)
// Write with TTL
await env.MY_KV.put('session:abc123', JSON.stringify({ userId: 1 }), {
expirationTtl: 3600, // auto-delete after 1 hour
});
// Read
const raw = await env.MY_KV.get('session:abc123');
const session = raw ? JSON.parse(raw) : null;
// Delete
await env.MY_KV.delete('session:abc123');
// List by prefix
const { keys } = await env.MY_KV.list({ prefix: 'session:' });
R2 (Object Storage)
// Upload (equivalent to Azure Blob Storage output binding)
await env.MY_BUCKET.put('uploads/image.png', request.body, {
httpMetadata: { contentType: 'image/png' },
customMetadata: { uploadedBy: 'user123' },
});
// Download
const object = await env.MY_BUCKET.get('uploads/image.png');
if (!object) return new Response('Not Found', { status: 404 });
return new Response(object.body, {
headers: { 'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream' },
});
// Delete
await env.MY_BUCKET.delete('uploads/image.png');
D1 (SQLite Edge DB)
// SELECT
const result = await env.MY_DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(userId).first<{ id: number; name: string; email: string }>();
// INSERT
await env.MY_DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
).bind('Taro Yamada', 'yamada@example.com').run();
// Batch execution (transaction equivalent)
await env.MY_DB.batch([
env.MY_DB.prepare('UPDATE orders SET status = ? WHERE id = ?').bind('shipped', orderId),
env.MY_DB.prepare('INSERT INTO order_logs (order_id, event) VALUES (?, ?)').bind(orderId, 'shipped'),
]);
Service Binding (Worker-to-Worker Calls)
Call another Worker internally without HTTP overhead — similar to Durable Functions CallActivity.
# wrangler.toml
[[services]]
binding = "AUTH_WORKER"
service = "auth-worker"
// Internal call to another Worker (no HTTP overhead)
const authResponse = await env.AUTH_WORKER.fetch(
new Request('https://internal/verify', {
method: 'POST',
body: JSON.stringify({ token }),
})
);
const { valid, userId } = await authResponse.json<{ valid: boolean; userId: string }>();
Deployment Methods
1. Manual Deploy via Wrangler CLI
# Deploy to production
wrangler deploy
# Deploy to a named environment (uses [env.staging] in wrangler.toml)
wrangler deploy --env staging
2. Automated Deploy via GitHub Actions
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Workers
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env production
3. Terraform (Infrastructure as Code)
Manage Workers as code, similar to Azure ARM Templates or Bicep.
# main.tf
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# Deploy Worker script
resource "cloudflare_worker_script" "api" {
account_id = var.account_id
name = "my-api-worker"
content = file("dist/index.js")
# Non-sensitive environment variable
plain_text_binding {
name = "API_BASE_URL"
text = "https://api.example.com"
}
# KV binding
kv_namespace_binding {
name = "MY_KV"
namespace_id = cloudflare_workers_kv_namespace.sessions.id
}
# R2 binding
r2_bucket_binding {
name = "MY_BUCKET"
bucket_name = cloudflare_r2_bucket.uploads.name
}
}
# KV Namespace
resource "cloudflare_workers_kv_namespace" "sessions" {
account_id = var.account_id
title = "my-sessions-kv"
}
# R2 Bucket
resource "cloudflare_r2_bucket" "uploads" {
account_id = var.account_id
name = "my-uploads"
location = "APAC"
}
# HTTP route (custom domain)
resource "cloudflare_worker_route" "api" {
zone_id = var.zone_id
pattern = "api.example.com/*"
script_name = cloudflare_worker_script.api.name
}
# Cron trigger
resource "cloudflare_worker_cron_trigger" "cleanup" {
account_id = var.account_id
script_name = cloudflare_worker_script.api.name
schedules = ["0 */6 * * *"]
}
D1 Migration Management
D1 provides a versioned migration mechanism: numbered SQL files are applied in order, and the applied state is tracked in a d1_migrations table.
EF Core Migrations is not a feature of Azure SQL Database — it is a feature of Entity Framework Core (.NET's ORM) and works across SQL Server / Azure SQL / PostgreSQL / SQLite, and more. There is no Azure SQL Database-specific "Migrations" feature.
The paradigms also differ:
- D1 migrations: hand-written SQL files applied in sequence — a SQL-script-based (database-first) approach, closer to DbUp / Flyway or SQL Database Projects (DACPAC / sqlpackage) in the .NET/Azure world.
- EF Core Migrations: a code-first approach that auto-generates migration code (
Up/Down) from diffs of your C# model classes.
Both apply versioned migrations sequentially, but treating them as equivalent is not accurate.
# Create a migration file
wrangler d1 migrations create my-database add_users_table
# → migrations/0001_add_users_table.sql is created
-- migrations/0001_add_users_table.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX idx_users_email ON users(email);
# Apply to local DB
wrangler d1 migrations apply my-database --local
# Apply to production DB
wrangler d1 migrations apply my-database
# Check migration status
wrangler d1 migrations list my-database
Migration Strategy for .NET Developers
Since Workers does not support C#, fully migrating .NET workloads to Workers is not realistic. The recommended approach is a hybrid architecture:
User
│
▼
[Cloudflare Workers — TypeScript]
├── Auth & session validation (JWT, KV session store)
├── Rate limiting & bot detection
├── Static asset delivery (R2)
├── Lightweight CRUD (D1)
└── Proxy heavy operations to Azure
│
▼
[Azure Functions — .NET 10]
├── Complex business logic
├── Azure SQL / Cosmos DB operations
├── Azure Service Bus processing
└── External API integration
Division of Responsibility
| Processing | Workers (TypeScript) | Azure Functions (.NET) |
|---|---|---|
| JWT validation | ✅ Great fit (instant edge validation) | — |
| Static file delivery | ✅ R2 + CDN | — |
| Lightweight KV caching | ✅ Workers KV | — |
| Simple CRUD (SQLite scale) | ✅ D1 | — |
| Complex business logic | — | ✅ Great fit |
| Azure SQL / Cosmos DB | ❌ No direct connection | ✅ Native bindings |
| Existing .NET libraries | ❌ Not supported | ✅ |
| Batch / long-running jobs | ❌ CPU time limit | ✅ |
Platform Limits
| Item | Free Plan | Paid Plan (Workers Paid) |
|---|---|---|
| Requests | 100K/day | 10M/month ($0.30 per additional 1M) |
| CPU time per request | 10ms | 30 seconds |
| Memory | 128MB | 128MB |
| Script size | 3MB (gzipped) | 10MB (gzipped) |
| KV value size | 25MB | 25MB |
| R2 object size | 5GB | 5GB |
| D1 database size | 500MB | 2GB |
| Concurrent Workers | Unlimited (Isolate isolation) | Unlimited |
Workers CPU time counts only active CPU usage — not time spent waiting on network I/O (fetch, D1 queries, KV reads). This is different from Azure Functions' execution time billing. For I/O-bound workloads, the 30-second CPU limit is rarely a constraint.