Skip to main content

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.

AspectAzure FunctionsCloudflare Workers
Execution locationSpecific region (japaneast, etc.)Nearest edge PoP to the user (300+ locations)
Cold startYes (Consumption plan: hundreds of ms to seconds)None (V8 Isolates are pre-warmed)
Scale outInstances are replicated (can be stateful)Isolate per request (stateless)
Max execution time10 min (Consumption) to unlimited (Premium/Dedicated)CPU time: 10ms free / 30s paid
Runtime.NET / Node.js / Python / Java / etc.JavaScript / TypeScript / Python / Rust
PricingExecution time × memoryRequests (free: 100K/day)
About .NET

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 TriggerWorkers HandlerPurpose
HTTP TriggerfetchHTTP request handling
Timer TriggerscheduledCron-based recurring execution
Queue Trigger (Service Bus)queueConsuming from Cloudflare Queues
Blob Trigger(none — R2 events via separate config)
Event Grid Trigger(none)
Email TriggeremailCloudflare Email Routing

Binding Comparison

Workers Bindings correspond to Azure Functions input/output bindings. They are accessed via the env object.

Azure Functions BindingWorkers BindingDescription
Blob Storage (in/out)R2BucketObject storage
Table StorageKVNamespaceKey-value store
Service BusQueueMessage queue
SQL DatabaseD1DatabaseSQLite edge DB
Cosmos DBKVNamespace / D1DatabaseChoose based on use case
SignalRDurableObjectWebSocket / 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.

Difference from Entity Framework Migrations (common misconception)

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

ProcessingWorkers (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

ItemFree PlanPaid Plan (Workers Paid)
Requests100K/day10M/month ($0.30 per additional 1M)
CPU time per request10ms30 seconds
Memory128MB128MB
Script size3MB (gzipped)10MB (gzipped)
KV value size25MB25MB
R2 object size5GB5GB
D1 database size500MB2GB
Concurrent WorkersUnlimited (Isolate isolation)Unlimited
CPU Time vs Wall-Clock Time

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.