Back to Blog
Hook Mesh Team

Webhooks in Serverless: Lambda, Vercel, and Cloudflare Workers

A comprehensive tutorial on implementing webhooks in serverless architectures. Learn how to send and receive webhooks on AWS Lambda, Vercel Functions, and Cloudflare Workers with practical code examples and best practices.

Webhooks in Serverless: Lambda, Vercel, and Cloudflare Workers

Webhooks in Serverless: Lambda, Vercel, and Cloudflare Workers

Serverless webhooks require handling cold starts, timeouts, and stateless environments. This tutorial covers AWS Lambda, Vercel Functions, and Cloudflare Workers with practical patterns and code examples. For framework-specific guidance, see Next.js webhooks and Express.js patterns.

Why Serverless for Webhooks?

Serverless is ideal for webhooks: automatic scaling for unpredictable traffic, pay-per-execution cost efficiency, no infrastructure maintenance, and edge deployment for low latency. Constraints include cold starts, execution timeouts, and stateless environments. See webhook implementation guides for language-specific patterns.

AWS Lambda: The Serverless Pioneer

AWS Lambda combined with API Gateway provides a robust foundation for webhooks.

Handler Structure

// handler.js
export const receiveWebhook = async (event, context) => {
  // Parse the incoming webhook payload
  const payload = JSON.parse(event.body);

  // Verify the webhook signature
  const signature = event.headers['x-webhook-signature'];
  if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return {
      statusCode: 401,
      body: JSON.stringify({ error: 'Invalid signature' })
    };
  }

  // Acknowledge receipt immediately
  // Process asynchronously to avoid timeouts
  await sendToQueue(payload);

  return {
    statusCode: 200,
    body: JSON.stringify({ received: true })
  };
};

function verifySignature(payload, signature, secret) {
  const crypto = require('crypto');
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  return signature === `sha256=${expected}`;
}

API Gateway Integration

To expose your Lambda as a webhook endpoint, configure API Gateway with the following CloudFormation or SAM template:

# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  WebhookFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: handler.receiveWebhook
      Runtime: nodejs18.x
      Timeout: 30
      MemorySize: 256
      Events:
        WebhookEndpoint:
          Type: Api
          Properties:
            Path: /webhooks
            Method: POST

Sending Webhooks from Lambda

When sending webhooks from Lambda, use a dedicated function triggered by events:

// sendWebhook.js
import https from 'https';

export const handler = async (event) => {
  const webhookUrl = event.targetUrl;
  const payload = event.payload;

  const response = await sendWithRetry(webhookUrl, payload, 3);

  return {
    statusCode: response.status,
    deliveryId: event.deliveryId
  };
};

async function sendWithRetry(url, payload, maxRetries) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Webhook-Signature': generateSignature(payload)
        },
        body: JSON.stringify(payload)
      });

      if (response.ok) return response;

      if (attempt < maxRetries) {
        await delay(Math.pow(2, attempt) * 1000);
      }
    } catch (error) {
      if (attempt === maxRetries) throw error;
      await delay(Math.pow(2, attempt) * 1000);
    }
  }
}

Cold Start Considerations

Cold starts add 100ms-seconds of latency. Mitigate by:

  • Using provisioned concurrency for critical webhook endpoints
  • Keeping deployment packages small
  • Choosing ARM64 architecture for faster initialization
  • Using Lambda SnapStart for Java workloads

Vercel Functions: Developer Experience First

Vercel offers serverless functions and edge functions—both work well for webhooks with different trade-offs.

Serverless Functions with Next.js

// app/api/webhooks/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('x-webhook-signature');

  // Verify signature
  const expectedSig = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(body)
    .digest('hex');

  if (signature !== `sha256=${expectedSig}`) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 401 }
    );
  }

  const payload = JSON.parse(body);

  // Queue for async processing
  await fetch(process.env.QUEUE_URL!, {
    method: 'POST',
    body: JSON.stringify(payload)
  });

  return NextResponse.json({ received: true });
}

export const config = {
  maxDuration: 30, // Maximum execution time in seconds
};

Edge Functions for Low Latency

Edge Functions run on Cloudflare's network with sub-millisecond cold starts:

// app/api/webhooks/edge/route.ts
import { NextRequest, NextResponse } from 'next/server';

export const runtime = 'edge';

export async function POST(request: NextRequest) {
  const payload = await request.json();
  const signature = request.headers.get('x-webhook-signature');

  // Edge-compatible signature verification
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(process.env.WEBHOOK_SECRET),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const signatureBuffer = await crypto.subtle.sign(
    'HMAC',
    key,
    encoder.encode(JSON.stringify(payload))
  );

  const expectedSig = Array.from(new Uint8Array(signatureBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');

  if (signature !== `sha256=${expectedSig}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Forward to processing endpoint
  await fetch('https://your-api.com/process', {
    method: 'POST',
    body: JSON.stringify(payload)
  });

  return NextResponse.json({ success: true });
}

Sending Webhooks from Vercel

// app/api/send-webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { targetUrl, eventType, data } = await request.json();

  const payload = {
    event: eventType,
    data,
    timestamp: new Date().toISOString()
  };

  const signature = await generateSignature(payload);

  const response = await fetch(targetUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': signature,
      'X-Webhook-Event': eventType
    },
    body: JSON.stringify(payload)
  });

  return NextResponse.json({
    delivered: response.ok,
    statusCode: response.status
  });
}

Cloudflare Workers: Edge-First Architecture

Cloudflare Workers run at the edge in 300+ data centers globally, ideal for low-latency webhook endpoints.

Basic Webhook Receiver

// src/index.js
export default {
  async fetch(request, env, ctx) {
    if (request.method !== 'POST') {
      return new Response('Method not allowed', { status: 405 });
    }

    const body = await request.text();
    const signature = request.headers.get('x-webhook-signature');

    // Verify signature using Web Crypto API
    const isValid = await verifySignature(body, signature, env.WEBHOOK_SECRET);
    if (!isValid) {
      return new Response('Unauthorized', { status: 401 });
    }

    const payload = JSON.parse(body);

    // Use waitUntil for async processing after response
    ctx.waitUntil(processWebhook(payload, env));

    return new Response(JSON.stringify({ received: true }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

async function verifySignature(body, signature, secret) {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(body));
  const expected = 'sha256=' + [...new Uint8Array(sig)]
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');

  return signature === expected;
}

async function processWebhook(payload, env) {
  // Store in Durable Objects or send to queue
  await env.WEBHOOK_QUEUE.send(payload);
}

Using Durable Objects for State

Durable Objects provide consistent state for stateless Workers:

// src/webhook-processor.js
export class WebhookProcessor {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }

  async fetch(request) {
    const payload = await request.json();

    // Track delivery attempts
    const attempts = await this.state.storage.get('attempts') || 0;
    await this.state.storage.put('attempts', attempts + 1);

    // Process with idempotency
    const processed = await this.state.storage.get(`processed:${payload.id}`);
    if (processed) {
      return new Response('Already processed', { status: 200 });
    }

    await this.processPayload(payload);
    await this.state.storage.put(`processed:${payload.id}`, true);

    return new Response('Processed', { status: 200 });
  }
}

Sending Webhooks with Queues

// src/queue-handler.js
export default {
  async queue(batch, env) {
    for (const message of batch.messages) {
      const { targetUrl, payload, retryCount = 0 } = message.body;

      try {
        const response = await fetch(targetUrl, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-Webhook-Signature': await sign(payload, env.SECRET)
          },
          body: JSON.stringify(payload)
        });

        if (!response.ok && retryCount < 5) {
          // Re-queue with exponential backoff
          message.retry({ delaySeconds: Math.pow(2, retryCount) * 60 });
        } else {
          message.ack();
        }
      } catch (error) {
        if (retryCount < 5) {
          message.retry({ delaySeconds: Math.pow(2, retryCount) * 60 });
        }
      }
    }
  }
};

Common Challenges and Solutions

Cold Starts: Use edge functions, provisioned concurrency, and small bundles.

Timeouts: Acknowledge quickly (1-2s), process asynchronously:

  • Lambda: SQS or EventBridge
  • Vercel: Background functions or external queues
  • Workers: waitUntil() or Cloudflare Queues

Statelessness: Use external databases (DynamoDB, PlanetScale), platform solutions (Durable Objects, Vercel KV), or idempotency keys.

Best Practices for Serverless Webhooks

  1. Respond fast, process later: Acknowledge within 1-2 seconds, process asynchronously.

  2. Verify signatures: Always validate to prevent spoofing. See webhook security best practices.

  3. Implement idempotency: Use idempotency keys to prevent duplicates. See webhook idempotency guide.

  4. Use queues: Decouple receipt from processing.

  5. Monitor and alert: Track delivery rates, latencies, and failures.

Simplify with Hook Mesh

Hook Mesh simplifies webhook infrastructure across all serverless platforms. Integrate with a single API call:

// Works on Lambda, Vercel, or Workers
const response = await fetch('https://api.hookmesh.com/v1/webhooks', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${HOOKMESH_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    event: 'order.completed',
    destination: customerId,
    payload: orderData
  })
});

Hook Mesh manages retries, monitoring, and dashboards.

Conclusion

Serverless platforms excel for webhooks: automatic scaling, cost efficiency, and low operational overhead. Lambda offers maturity and AWS integration, Vercel offers developer experience, and Cloudflare Workers offer edge-first architecture. Success requires understanding constraints: acknowledge quickly, process asynchronously, and leverage platform features. See scaling webhooks 0-10K for growth guidance.

Related Posts