Cloudflare Workers: The Complete Cheatsheet

Introduction: What are Cloudflare Workers and Why They Matter

Cloudflare Workers are serverless JavaScript functions that run at the edge of Cloudflare’s global network across 300+ locations worldwide. They allow developers to build applications that respond to HTTP requests without managing servers. Cloudflare Workers matter because they provide ultra-low latency, high reliability, and automatic scaling while reducing costs and complexity. By executing code closer to your users, Workers deliver faster user experiences through Cloudflare’s edge computing platform.

Core Concepts and Fundamentals

ConceptDescription
Edge ComputingCode execution close to users at Cloudflare’s data centers worldwide
ServerlessNo server management or provisioning needed
V8 IsolatesLightweight JavaScript execution environments with microsecond cold starts
Workers KVGlobal, low-latency key-value data store for Workers
Durable ObjectsGlobally unique objects providing coordination and consistent storage
R2Object storage service compatible with S3 API
D1Serverless SQLite database for Workers
QueuesManaged message queue system for asynchronous processing
Workers TriggersEvent-based execution (HTTP, Cron, etc.)

Getting Started with Cloudflare Workers

Prerequisites

  • Cloudflare account
  • Node.js installed
  • Wrangler CLI installed (npm install -g wrangler)

Basic Setup Process

  1. Install Wrangler and authenticate

    npm install -g wrangler
    wrangler login
    
  2. Create a new Worker project

    wrangler init my-worker
    cd my-worker
    
  3. Develop and test locally

    wrangler dev
    
  4. Deploy to Cloudflare

    wrangler publish
    

Writing Your First Worker

Simple Response Worker

export default {
  async fetch(request, env, ctx) {
    return new Response("Hello World!");
  }
};

Handling Different HTTP Methods

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    switch (request.method) {
      case "GET":
        return handleGet(url);
      case "POST":
        return handlePost(request);
      default:
        return new Response("Method not allowed", { status: 405 });
    }
  }
};

async function handleGet(url) {
  // Handle GET logic
  return new Response("GET request received");
}

async function handlePost(request) {
  // Handle POST logic
  const data = await request.json();
  return new Response(JSON.stringify({ received: data }), {
    headers: { "Content-Type": "application/json" }
  });
}

Worker Configuration

wrangler.toml Configuration

name = "my-worker"
main = "src/index.js"
compatibility_date = "2023-10-30"

# Environment variables
[vars]
API_KEY = "your-api-key-for-dev"

# Production environment
[env.production]
vars = { API_KEY = "your-api-key-for-prod" }

# Route binding
[triggers]
routes = [
  { pattern = "example.com/api/*", zone_name = "example.com" }
]

# KV Namespace binding
[[kv_namespaces]]
binding = "MY_KV"
id = "xxxx"

# R2 Bucket binding
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"

# D1 Database binding
[[d1_databases]]
binding = "MY_DB"
database_name = "my-database"
database_id = "xxxx"

Environment Variables and Secrets

# Set environment variable
wrangler secret put API_KEY

# Access in code
export default {
  async fetch(request, env, ctx) {
    const apiKey = env.API_KEY;
    // Use apiKey
  }
};

Common Patterns and Examples

Modifying Requests and Responses

export default {
  async fetch(request, env, ctx) {
    // Clone the request to modify it
    const modifiedRequest = new Request(request);
    modifiedRequest.headers.set("X-Custom-Header", "custom-value");
    
    // Forward to origin
    const response = await fetch(modifiedRequest);
    
    // Clone the response to modify it
    const modifiedResponse = new Response(response.body, response);
    modifiedResponse.headers.set("X-Served-By", "Cloudflare-Workers");
    
    return modifiedResponse;
  }
};

Caching Responses

export default {
  async fetch(request, env, ctx) {
    const cacheKey = new Request(request.url, request);
    const cache = caches.default;
    
    // Check if the request is in the cache
    let response = await cache.match(cacheKey);
    
    if (!response) {
      // If not in cache, fetch from origin
      response = await fetch(request);
      
      // Clone the response for caching
      const responseToCache = new Response(response.body, response);
      
      // Set cache control headers
      responseToCache.headers.set('Cache-Control', 'max-age=3600');
      
      // Store response in cache
      ctx.waitUntil(cache.put(cacheKey, responseToCache));
    }
    
    return response;
  }
};

API Gateway / Proxy

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const apiBase = 'https://api.example.com';
    const apiPath = url.pathname.replace('/api', '');
    
    // Create a new request to the API
    const apiRequest = new Request(`${apiBase}${apiPath}`, {
      method: request.method,
      headers: request.headers,
      body: request.body,
    });
    
    // Add API key
    apiRequest.headers.set('Authorization', `Bearer ${env.API_KEY}`);
    
    try {
      return await fetch(apiRequest);
    } catch (err) {
      return new Response(`API Error: ${err.message}`, { status: 500 });
    }
  }
};

Working with Data Services

Workers KV (Key-Value Store)

// Reading data
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const key = url.pathname.split('/').pop();
    
    // Read from KV
    const value = await env.MY_KV.get(key);
    if (value === null) {
      return new Response('Not found', { status: 404 });
    }
    
    return new Response(value);
  }
};

// Writing data
export default {
  async fetch(request, env, ctx) {
    if (request.method === 'POST') {
      const key = new URL(request.url).pathname.split('/').pop();
      const value = await request.text();
      
      // Write to KV (with optional expiration in seconds)
      await env.MY_KV.put(key, value, { expirationTtl: 86400 });
      
      return new Response('Stored successfully');
    }
    
    return new Response('Method not allowed', { status: 405 });
  }
};

Durable Objects

// Durable Object class
export class Counter {
  constructor(state) {
    this.state = state;
  }

  async fetch(request) {
    // Get current count
    let count = await this.state.storage.get("count") || 0;
    
    // Update count based on request
    if (request.method === "POST") {
      count++;
      await this.state.storage.put("count", count);
    }
    
    return new Response(count.toString());
  }
}

// Worker that uses the Durable Object
export default {
  async fetch(request, env, ctx) {
    // Create Durable Object ID based on a name
    const id = env.COUNTER.idFromName("my-counter");
    // Get Durable Object stub using the ID
    const obj = env.COUNTER.get(id);
    // Forward the request to the Durable Object
    return await obj.fetch(request);
  }
};

// In wrangler.toml:
// [[durable_objects]]
// bindings = [{ name = "COUNTER", class_name = "Counter" }]

R2 Storage

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const objectKey = url.pathname.replace('/storage/', '');
    
    if (request.method === 'GET') {
      // Get object from R2
      const object = await env.MY_BUCKET.get(objectKey);
      
      if (object === null) {
        return new Response('Object Not Found', { status: 404 });
      }
      
      const headers = new Headers();
      object.writeHttpMetadata(headers);
      headers.set('etag', object.httpEtag);
      
      return new Response(object.body, {
        headers
      });
    }
    
    if (request.method === 'PUT') {
      // Store object in R2
      await env.MY_BUCKET.put(objectKey, request.body, {
        httpMetadata: request.headers
      });
      
      return new Response('Object uploaded successfully');
    }
    
    return new Response('Method not allowed', { status: 405 });
  }
};

D1 Database

export default {
  async fetch(request, env, ctx) {
    // Create table
    if (request.url.includes('/setup')) {
      await env.MY_DB.exec(`
        CREATE TABLE IF NOT EXISTS users (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT,
          email TEXT
        )
      `);
      return new Response('Table created');
    }
    
    // Insert data
    if (request.method === 'POST') {
      const { name, email } = await request.json();
      
      const stmt = env.MY_DB.prepare(
        'INSERT INTO users (name, email) VALUES (?, ?)'
      );
      
      const result = await stmt.bind(name, email).run();
      
      return new Response(JSON.stringify({ 
        success: true, 
        id: result.meta.last_row_id 
      }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    // Query data
    if (request.method === 'GET') {
      const { results } = await env.MY_DB.prepare(
        'SELECT * FROM users'
      ).all();
      
      return new Response(JSON.stringify(results), {
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    return new Response('Method not allowed', { status: 405 });
  }
};

Workers for Advanced Use Cases

HTML Rewriting

export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);
    
    // Check if HTML
    const contentType = response.headers.get('content-type') || '';
    if (!contentType.includes('text/html')) {
      return response;
    }
    
    // Create HTML rewriter
    return new HTMLRewriter()
      .on('title', {
        element(element) {
          element.setInnerContent('Modified Title');
        }
      })
      .on('a', {
        element(element) {
          // Add rel="noopener" to all links
          element.setAttribute('rel', 'noopener');
        }
      })
      .on('div#hero', {
        element(element) {
          element.setAttribute('class', 'enhanced-hero');
        },
        text(text) {
          // Replace text content
          if (text.text.includes('Original Text')) {
            text.replace('New Text');
          }
        }
      })
      .transform(response);
  }
};

Scheduled Tasks (Cron Triggers)

// Worker executed on a schedule
export default {
  // Standard request handler
  async fetch(request, env, ctx) {
    return new Response('Hello from scheduled worker');
  },
  
  // Scheduled handler
  async scheduled(event, env, ctx) {
    // This runs according to the cron schedule
    console.log('Running scheduled task', event.cron);
    
    // Perform scheduled operations
    await performCleanup(env);
    
    // Make API calls
    const response = await fetch('https://api.example.com/update', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ timestamp: Date.now() })
    });
    
    console.log('Task completed with status:', response.status);
  }
};

async function performCleanup(env) {
  // Implementation of cleanup task
}

// In wrangler.toml:
// [triggers]
// crons = ["0 0 * * *"] # Daily at midnight UTC

Queues

// Producer Worker
export default {
  async fetch(request, env, ctx) {
    if (request.method === 'POST') {
      const message = await request.json();
      
      // Send message to queue
      await env.MY_QUEUE.send(message);
      
      return new Response('Message queued');
    }
    
    return new Response('Method not allowed', { status: 405 });
  }
};

// Consumer Worker
export default {
  async queue(batch, env, ctx) {
    for (const message of batch.messages) {
      try {
        // Process message
        console.log('Processing message:', message.body);
        
        // Perform work with the message
        await processMessage(message.body);
        
        // If processing succeeded, the message will be deleted from the queue
      } catch (error) {
        // If an error is thrown, the message will be retried
        console.error('Error processing message:', error);
        throw error;
      }
    }
  }
};

async function processMessage(data) {
  // Message processing logic
}

// In wrangler.toml:
// [[queues.consumers]]
// queue = "my-queue"
// max_batch_size = 10
// max_batch_timeout = 5
// max_retries = 3

Security Best Practices

Authentication and Authorization

export default {
  async fetch(request, env, ctx) {
    // Check for authentication token
    const authHeader = request.headers.get('Authorization');
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return new Response('Unauthorized', { status: 401 });
    }
    
    const token = authHeader.split(' ')[1];
    
    // Validate token (example)
    if (!isValidToken(token, env.API_SECRET)) {
      return new Response('Forbidden', { status: 403 });
    }
    
    // Continue with authorized request
    return handleAuthorizedRequest(request, env);
  }
};

function isValidToken(token, secret) {
  // Token validation logic
  // In production, use proper JWT validation or other secure method
  return token === 'valid-token'; // Example only
}

async function handleAuthorizedRequest(request, env) {
  // Process authorized request
  return new Response('Authorized content');
}

CORS Configuration

export default {
  async fetch(request, env, ctx) {
    // Handle CORS preflight requests
    if (request.method === 'OPTIONS') {
      return handleCors(request, true);
    }
    
    // Handle actual request
    const response = await handleRequest(request, env);
    
    // Add CORS headers to response
    return handleCors(request, false, response);
  }
};

function handleCors(request, isPreflight, response) {
  const origin = request.headers.get('Origin');
  const allowedOrigins = ['https://example.com', 'https://sub.example.com'];
  
  // Check if origin is allowed
  const corsHeaders = {
    'Access-Control-Allow-Origin': allowedOrigins.includes(origin) ? origin : 'https://example.com',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Access-Control-Max-Age': '86400', // 24 hours
  };
  
  // Handle preflight request
  if (isPreflight) {
    return new Response(null, {
      status: 204,
      headers: corsHeaders,
    });
  }
  
  // Add CORS headers to actual response
  const newResponse = new Response(response.body, response);
  Object.entries(corsHeaders).forEach(([key, value]) => {
    newResponse.headers.set(key, value);
  });
  
  return newResponse;
}

async function handleRequest(request, env) {
  // Actual request handling logic
  return new Response('Response data');
}

Content Security Policies

export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);
    
    // Add security headers
    const secureResponse = new Response(response.body, response);
    secureResponse.headers.set('Content-Security-Policy', "default-src 'self'; script-src 'self' https://trusted-scripts.com;");
    secureResponse.headers.set('X-Content-Type-Options', 'nosniff');
    secureResponse.headers.set('X-Frame-Options', 'DENY');
    secureResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
    secureResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
    secureResponse.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
    
    return secureResponse;
  }
};

Common Challenges and Solutions

ChallengeSolution
Cold start performanceCloudflare Workers have microsecond cold starts due to V8 isolates; generally not an issue
CPU-intensive tasksSplit work into smaller chunks; use Queues for async processing; stay within CPU limits
Large responsesStream large responses; use R2 for static content; implement chunked processing
Rate limitingImplement token bucket algorithm with Workers KV or Durable Objects
DebuggingUse console.log(), wrangler tail for logs, and local dev environment with wrangler dev
Cross-origin requestsImplement proper CORS headers; use Workers to proxy API requests
Global state managementUse Durable Objects for global coordination; KV for shared read-mostly data
Database connectionsUse D1 for SQL needs; design for serverless patterns (connection pooling doesn’t apply)

Performance Optimization Tips

Worker Optimization

  • Keep dependencies minimal
  • Avoid large libraries that increase bundle size
  • Use native Web APIs when possible
  • Leverage streaming for large responses
  • Use waitUntil() for non-critical background tasks
  • Cache expensive computations or API responses

KV Optimization

  • Use KV for read-heavy workloads
  • Batch operations when possible
  • Structure keys efficiently for list operations
  • Be aware of KV consistency model (eventual consistency)
  • Use cache API for ultra-low latency needs

Request/Response Handling

export default {
  async fetch(request, env, ctx) {
    // Example: Stream a large response
    return streamLargeResponse(request, env);
  }
};

async function streamLargeResponse(request, env) {
  const { readable, writable } = new TransformStream();
  
  ctx.waitUntil((async () => {
    const writer = writable.getWriter();
    try {
      // Get data in chunks and write to stream
      const data = await getDataInChunks(env);
      for (const chunk of data) {
        await writer.write(chunk);
      }
    } finally {
      await writer.close();
    }
  })());
  
  return new Response(readable, {
    headers: { 'Content-Type': 'application/json' }
  });
}

async function getDataInChunks(env) {
  // Implementation to get data in chunks
  // Could be from R2, external API, etc.
}

Deployment and CI/CD

GitHub Actions Integration

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: npm ci
      - name: Publish
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          accountId: ${{ secrets.CF_ACCOUNT_ID }}
          command: publish

Testing Before Deployment

# Run tests
npm test

# Test in local development mode
wrangler dev

# Test in preview mode (deploys to workers.dev subdomain)
wrangler publish --env staging

Resources for Further Learning

Official Documentation

Community Resources

  • Cloudflare Developer Discord
  • Workers GitHub repository
  • Cloudflare Community forums

Wrangler CLI Command Reference

# Create new project
wrangler init my-worker

# Start local development
wrangler dev

# Deploy to production
wrangler publish

# Deploy to specific environment
wrangler publish --env production

# View logs
wrangler tail

# Generate types for bindings
wrangler types

# Secret management
wrangler secret put MY_SECRET
wrangler secret delete MY_SECRET

# KV operations
wrangler kv:namespace create "MY_NAMESPACE"
wrangler kv:key put --namespace-id=xxx "key" "value"
wrangler kv:key get --namespace-id=xxx "key"

# R2 operations
wrangler r2 bucket create my-bucket

# D1 operations
wrangler d1 create my-database
wrangler d1 execute my-database --command="CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"

This cheatsheet provides a comprehensive overview of Cloudflare Workers, covering everything from basic concepts to advanced techniques, with practical code examples and best practices for both beginners and experienced developers.

Scroll to Top