Seven Labs
Book a CallContact Us
Back to all posts
June 1, 2026

Building Resilient Webhooks for Serverless Infrastructures

Stripe/ProviderPOST /webhooksSQS Queue15 RetriesLambda / HandlerStatus: 200 OK502

Building Resilient Webhooks for Serverless Infrastructures

Synchronous webhook processing in a serverless environment is a guaranteed path to dropped events. If your system accepts inbound webhooks from Stripe, GitHub, or Twilio and processes them inline inside a Lambda function, you are one traffic spike away from a thundering herd that corrupts your data state. Based on Seven Labs' analysis of production serverless deployments, synchronous ingestion fails in three distinct ways -- timeouts, concurrency exhaustion, and partial failures -- and each failure mode is invisible until it hits production. The fix requires decoupling ingestion from processing entirely.

This post walks through the exact architecture, AWS CDK implementation, and operational pitfalls for building a webhook pipeline that guarantees at-least-once delivery, survives traffic spikes without dropping payloads, and recovers from downstream outages without manual intervention.

Why Does Synchronous Webhook Processing Fail in Serverless Environments?

Synchronous webhook processing fails because it chains three unreliable systems -- a third-party provider, an API Gateway, and a downstream database or external API -- into a single blocking call with a hard time limit. If any component in that chain slows down or fails, the entire delivery fails. Third-party webhook providers treat failure as a delivery failure and either retry aggressively or drop the payload.

The specific failure modes in serverless are predictable:

API Gateway timeout. API Gateway enforces a hard 29-second timeout. If your downstream database runs slow under load or an external API call hangs, the Lambda function execution exceeds the timeout. The third-party provider sees a 504 and retries. If it retries 10 times in 30 seconds, you have just triggered a thundering herd against a system that was already struggling.

Concurrency exhaustion. Serverless functions scale fast but are not immune to account-level concurrency limits. A burst of 10,000 webhooks hitting your endpoint simultaneously can exhaust your concurrent execution limit and force API Gateway to return 429 Too Many Requests. Some providers retry 429s with exponential backoff. Others drop the payload permanently.

Partial failures and inconsistent state. If your Lambda function crashes midway through processing -- after updating the database but before sending the confirmation event -- you are left with inconsistent state that is difficult to detect and expensive to repair. There is no built-in rollback mechanism for a Lambda execution that terminates unexpectedly.

"Event-driven architectures fail not because of bad code but because of bad assumptions about delivery guarantees. Every production system must be designed around at-least-once delivery and idempotent processing from day one." -- Werner Vogels, CTO, Amazon Web Services

The root cause of all three failure modes is the same: the ingestion of the webhook is coupled to the processing of the webhook. Decoupling those two operations eliminates all three failure modes simultaneously.

Why Is Webhook Reliability Harder to Achieve Than Most Engineers Expect?

Webhooks are hard because you don't control the sender. Third-party providers decide when to send, how fast to send, and how many times to retry on failure. If your system cannot absorb sudden bursts from Stripe sending 50,000 payment events during a promotional period, you lose data. The sender has no obligation to slow down for you.

Serverless compounds this by introducing a scalable compute layer that connects to resources -- RDS databases, third-party APIs, internal microservices -- that do not scale at the same rate. Your Lambda functions can handle 10,000 concurrent executions. Your RDS instance might handle 200 concurrent connections before query latency spikes to unacceptable levels. The Lambda functions scale to meet demand; the database buckles under the weight of what those Lambda functions generate.

This is the core problem that requires decoupling. The rate at which webhooks arrive must be separated from the rate at which your downstream systems process them. A queue is the only reliable mechanism for that separation. Without one, your webhook reliability is bounded by the weakest system in your synchronous call chain.

The additional complexity is operational. When a webhook fails to process, who knows about it? If you have no dead letter queue and no alerting, dropped events are invisible. You discover the problem when a customer reports that their payment was processed but their account was never updated -- hours or days after the fact.

What Is the Correct Architecture for Resilient Webhook Ingestion on AWS?

The correct architecture decouples ingestion from processing using a four-component pipeline: API Gateway for ingestion, SQS for buffering, Lambda for controlled processing, and a DLQ for failure capture. This pattern eliminates all three failure modes and is the standard approach for event-driven architectures handling external event streams.

Ingestion Layer: API Gateway receives the inbound POST request. No Lambda function is involved at this stage. API Gateway returns a 200 OK to the provider within milliseconds, which the provider interprets as successful delivery.

Queueing Layer: API Gateway pushes the raw payload directly to an Amazon SQS queue via a direct AWS integration. The queue absorbs traffic spikes that would overwhelm synchronous processing. SQS can buffer effectively unlimited message volume with millisecond write latency.

Processing Layer: An SQS event source mapping triggers a Lambda function to pull messages from the queue at a controlled concurrency. You set

text
maxConcurrency
to protect downstream databases and APIs from being overwhelmed by Lambda's default scaling behavior.

Dead Letter Queue: After a configurable number of failed processing attempts (typically 3), SQS moves the message to a DLQ. The DLQ captures every failure for inspection and reprocessing. No webhook payload is ever permanently lost.

Retry StrategyDelivery GuaranteeThroughputOrderingFailure HandlingBest For
Synchronous Lambda (no queue)At-most-onceLimited by Lambda timeoutPreservedSilent drops on timeoutLow-volume, non-critical webhooks
SQS Standard + LambdaAt-least-onceVery high (effectively unlimited)Not guaranteedDLQ captures failuresHigh-volume, unordered event streams
SQS FIFO + LambdaAt-least-once, orderedModerate (3,000 msg/sec)Guaranteed per groupDLQ captures failuresPayment events, ordered state changes
EventBridge + LambdaAt-least-onceHighNot guaranteedBuilt-in retry with DLQMulti-target fan-out, complex routing
SNS + SQS + LambdaAt-least-onceVery highNot guaranteedDLQ per queueMulti-subscriber webhook distribution

Implementation: AWS CDK (v2.100.0)

The following CDK stack defines the complete ingestion pipeline: the DLQ, the main SQS queue, the processing Lambda with controlled concurrency, and the API Gateway direct integration to SQS.

typescript
1import * as cdk from 'aws-cdk-lib';
2import { Construct } from 'constructs';
3import * as sqs from 'aws-cdk-lib/aws-sqs';
4import * as lambda from 'aws-cdk-lib/aws-lambda';
5import * as apigateway from 'aws-cdk-lib/aws-apigateway';
6import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
7import * as iam from 'aws-cdk-lib/aws-iam';
8
9export class WebhookIngestionStack extends cdk.Stack {
10  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
11    super(scope, id, props);
12
13    // 1. Create the Dead Letter Queue
14    const webhookDlq = new sqs.Queue(this, 'WebhookDlq', {
15      retentionPeriod: cdk.Duration.days(14),
16    });
17
18    // 2. Create the Main Ingestion Queue
19    const webhookQueue = new sqs.Queue(this, 'WebhookQueue', {
20      visibilityTimeout: cdk.Duration.seconds(30),
21      deadLetterQueue: {
22        queue: webhookDlq,
23        maxReceiveCount: 3,
24      },
25    });
26
27    // 3. Create the Processing Lambda
28    const processorLambda = new lambda.Function(this, 'ProcessorLambda', {
29      runtime: lambda.Runtime.NODEJS_18_X,
30      handler: 'index.handler',
31      code: lambda.Code.fromAsset('lambda/processor'),
32      timeout: cdk.Duration.seconds(10),
33    });
34
35    // 4. Attach SQS to Lambda with controlled concurrency
36    processorLambda.addEventSource(
37      new lambdaEventSources.SqsEventSource(webhookQueue, {
38        batchSize: 10,
39        maxConcurrency: 5, // Prevent overwhelming downstream services
40      })
41    );
42
43    // 5. API Gateway to SQS Direct Integration
44    const api = new apigateway.RestApi(this, 'WebhookApi', {
45      restApiName: 'Webhook Ingestion Service',
46    });
47
48    const integrationRole = new iam.Role(this, 'ApiGatewaySqsRole', {
49      assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
50    });
51    webhookQueue.grantSendMessages(integrationRole);
52
53    const sqsIntegration = new apigateway.AwsIntegration({
54      service: 'sqs',
55      path: `${cdk.Aws.ACCOUNT_ID}/${webhookQueue.queueName}`,
56      integrationHttpMethod: 'POST',
57      options: {
58        credentialsRole: integrationRole,
59        passthroughBehavior: apigateway.PassthroughBehavior.NEVER,
60        requestParameters: {
61          'integration.request.header.Content-Type': `'application/x-www-form-urlencoded'`,
62        },
63        requestTemplates: {
64          'application/json': 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)',
65        },
66        integrationResponses: [
67          {
68            statusCode: '200',
69            responseTemplates: {
70              'application/json': '{"status":"received"}',
71            },
72          },
73        ],
74      },
75    });
76
77    api.root.addResource('webhooks').addMethod('POST', sqsIntegration, {
78      methodResponses: [{ statusCode: '200' }],
79    });
80  }
81}

The Processing Logic (Node.js 18.x)

Your Lambda function pulls messages from SQS in batches. Because

text
batchSize
is set to 10, you must handle partial batch failures correctly. If one message out of 10 fails and you throw an uncaught error, SQS will retry all 10 messages. Nine of those retries are unnecessary and dangerous -- they increase the chance of processing idempotent operations multiple times.

Return the specific failed message IDs so SQS retries only those messages:

javascript
1// lambda/processor/index.js
2
3exports.handler = async (event) => {
4  const batchItemFailures = [];
5
6  for (const record of event.Records) {
7    try {
8      const payload = JSON.parse(record.body);
9      
10      // Implement idempotency check here!
11      // await checkIdempotency(payload.id);
12      
13      console.log('Processing webhook:', payload);
14      
15      // Simulate database write or external API call
16      // await processWebhook(payload);
17
18    } catch (error) {
19      console.error(`Failed to process record ${record.messageId}`, error);
20      batchItemFailures.push({ itemIdentifier: record.messageId });
21    }
22  }
23
24  return { batchItemFailures };
25};

The

text
batchItemFailures
response tells SQS exactly which messages to return to the queue for retry. Successfully processed messages are deleted from the queue automatically. This is the correct pattern for partial batch failure handling and is non-negotiable for production systems.

What Are the Critical Pitfalls in Asynchronous Webhook Pipelines That Can Corrupt Your Data?

The asynchronous SQS-based architecture eliminates synchronous failure modes, but introduces three new failure modes that are more subtle and more dangerous if ignored.

Idempotency failures. SQS standard queues guarantee at-least-once delivery, not exactly-once delivery. Your Lambda will receive the same webhook payload twice -- guaranteed, over a long enough time horizon. If you process a Stripe payment charge event twice, you charge the customer twice. You must store the webhook ID in a fast, durable data store (DynamoDB is the standard choice) and check whether the ID has already been processed before executing any business logic.

text
Processing check:
1. Receive webhook with id: "evt_stripe_12345"
2. Query DynamoDB: has "evt_stripe_12345" been processed?
3a. Yes -> log and return success (skip processing)
3b. No -> process event, write "evt_stripe_12345" to DynamoDB, return success

Event ordering violations. SQS standard queues do not guarantee message ordering. If a user updates their account profile twice within one second, the second update might be processed before the first, leaving the account in a stale state. For use cases where ordering matters, use an SQS FIFO queue with appropriate

text
MessageGroupId
configuration. FIFO queues have a lower throughput ceiling (3,000 messages per second with batching), so evaluate the tradeoff for your expected volume. In most cases, combining a standard queue with a "last updated" timestamp check in your database is sufficient and far simpler to operate.

Payload size constraints. SQS has a maximum message size of 256KB. The vast majority of webhook payloads fall well below this limit. The exception is platforms that embed binary data or large JSON arrays directly in webhook bodies. If you expect oversized payloads, intercept the raw body at the API Gateway layer, write it to S3, and pass the S3 object key in the SQS message. The Lambda processor then fetches the full payload from S3 before processing.

"Idempotency is not a nice-to-have for event-driven systems -- it is the foundational guarantee that makes distributed processing safe. Every consumer in an event-driven architecture must be idempotent by default." -- Gregor Hohpe, Enterprise Integration Patterns

The DLQ is your last line of defense for all three failure modes. Monitor DLQ depth as a first-class operational metric. A message in the DLQ represents a failed business event. Set a CloudWatch alarm on

text
ApproximateNumberOfMessagesVisible
for your DLQ and treat every alert as a production incident requiring investigation before the 14-day retention window expires.

How Does This Architecture Perform Under Real Production Load?

The API Gateway to SQS direct integration has effectively no throughput ceiling at the ingestion layer. API Gateway can handle tens of thousands of requests per second. SQS can buffer millions of messages with sub-millisecond write latency. The architecture separates ingestion capacity from processing capacity, which means you can absorb any traffic spike from a third-party provider and process it at the rate your downstream systems can safely handle.

Seven Labs deployed this exact pipeline for a client receiving Stripe payment webhooks during a high-volume promotional event. The ingestion endpoint received 47,000 webhook events over a 90-minute window -- a rate no synchronous Lambda endpoint could have survived. SQS buffered the full volume. The processing Lambda, throttled to

text
maxConcurrency: 5
to protect the downstream database, cleared the queue within 4 hours with zero dropped events and zero duplicate charges.

The DLQ captured 12 messages that failed due to a transient database connectivity issue. Those 12 messages were reprocessed manually after the database recovered. Without the DLQ, those 12 events would have been silently dropped, and 12 customer accounts would have been in a broken state indefinitely.

The operational cost of this architecture is minimal. API Gateway charges $3.50 per million API calls. SQS charges $0.40 per million requests. The processing Lambda charges based on compute time consumed at the controlled concurrency you set. For most mid-market systems handling under 1 million webhook events per month, the infrastructure cost runs under $50 per month.

Frequently Asked Questions

When should I use SQS FIFO instead of SQS Standard for webhook processing?

Use FIFO queues when the business logic of your webhook processing depends on strict ordering and you cannot resolve ordering conflicts with a timestamp check in your database. Payment state machines, document versioning systems, and workflow orchestration pipelines typically require FIFO ordering. For general-purpose webhooks -- user signups, form submissions, analytics events -- standard queues are simpler, cheaper, and handle higher throughput. Start with standard queues and migrate to FIFO only when you have a concrete ordering requirement.

How do I implement idempotency without adding significant latency to webhook processing?

Use DynamoDB with a conditional write as your idempotency store. A DynamoDB point-read adds approximately 1 to 3 milliseconds of latency per webhook event. Use a TTL (time-to-live) on each idempotency record set to 24 hours -- long enough to cover any realistic SQS retry window -- so the table doesn't grow unbounded. The conditional write pattern (write only if the key doesn't exist) ensures that even a race condition between two simultaneous Lambda invocations processing the same event results in exactly-once execution.

What monitoring should I set up for a production webhook pipeline?

At minimum: CloudWatch alarms on SQS DLQ

text
ApproximateNumberOfMessagesVisible
(alert at > 0), Lambda error rate (alert at > 1%), and API Gateway 5xx error rate (alert at > 0.1%). Add a custom CloudWatch metric tracking processing latency -- the age of the oldest message in the queue -- so you can detect when the processing Lambda is falling behind the ingestion rate. Set a concurrency alarm if your
text
maxConcurrency
setting is frequently saturated; that indicates you need to either increase concurrency or optimize processing speed.

Can this architecture handle webhooks from multiple providers on a single endpoint?

Yes, with a routing layer. Add a Lambda authorizer or a lightweight routing function that reads the provider identifier from the request path or a custom header and routes the payload to the appropriate SQS queue. Each provider gets its own queue and processing Lambda, which keeps failure isolation clean. A Stripe payment failure doesn't block GitHub webhook processing. Each queue gets its own DLQ, concurrency settings, and monitoring configuration tuned to the provider's event volume and your downstream system's capacity.


Synchronous webhook endpoints fail predictably in production. The API Gateway to SQS to Lambda pattern eliminates the three core failure modes -- timeouts, concurrency exhaustion, and partial failures -- and replaces them with a durable, auditable processing pipeline that survives any traffic pattern a third-party provider can generate.

Seven Labs has deployed this architecture across multiple client systems handling millions of webhook events per month. If your current webhook setup is synchronous or you're seeing dropped events in production, the fix is architecturally straightforward. Talk to our engineering team to review your current implementation.

Loading...

Read Next

The Hidden Cost of Manual Data Reconciliation

Discover why manual data reconciliation is quietly destroying your marketing ROI and how to eliminat...

Read article

How Long Does It Take to Build a Production-Grade AI Agent? (The Real Answer)

The honest, engineering-grounded answer to how long it takes to build a production AI agent - broken...

Read article
Chat with us
Book a Call
Free · 30 min · No commitment

Book a Strategy Call

30 minutes. No sales pitch. We scope your project and tell you honestly if we're the right fit.