Skip to main content

Webhook Load Testing Guide

Phase 3 Sprint 5: Performance Validation

Load testing procedures for Stripe webhook endpoints to ensure production readiness.


Test Objectives

  1. Throughput: Handle 100+ webhooks/minute
  2. Response Time: < 500ms per webhook (95th percentile)
  3. Reliability: 0% error rate under normal load
  4. Resilience: Graceful degradation under peak load
  5. Idempotency: Correctly handle duplicate webhooks

Test Environment

Local Development Setup

# Start services
docker-compose up -d

# Verify all services healthy
docker-compose ps

# Check baseline resource usage
docker stats --no-stream
# Use docker-compose.prod.yml
docker-compose -f docker-compose.prod.yml up -d

# Scale producer service
docker-compose -f docker-compose.prod.yml up -d --scale producer=3

# Configure load balancer (if using)

Monitoring Setup

# Terminal 1: Producer logs
docker-compose logs -f producer

# Terminal 2: MongoDB stats
docker exec -it automation-mongodb mongosh
> use admin
> db.runCommand({serverStatus: 1})

# Terminal 3: Resource monitor
docker stats

# Terminal 4: Test execution
# Run load tests here

Load Testing Tools

Option 1: Apache Bench (Simple)

# Install
sudo apt install apache2-utils

# Basic test
ab -n 1000 -c 10 \
-H "Content-Type: application/json" \
-H "stripe-signature: test" \
http://localhost:3000/api/webhooks/test
# Install
npm install -g artillery

# Create test script
artillery run webhook-load-test.yml

Option 3: k6 (Advanced)

# Install
sudo apt install k6

# Run test
k6 run webhook-load-test.js

Test Scenarios

Scenario 1: Baseline Performance

Objective: Measure webhook processing time under no load.

Test Script: webhook-baseline.yml

config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 1 # 1 request/second
name: "Baseline"

scenarios:
- name: "Subscription Created"
flow:
- post:
url: "/api/webhooks/test"
json:
type: "customer.subscription.created"
data:
object:
id: "sub_{{ $randomString() }}"
customer: "cus_test"
status: "active"
headers:
stripe-signature: "dummy" # Update with real signature for prod

Run Test:

artillery run webhook-baseline.yml

Expected Results:

  • Response time: < 200ms (p95)
  • Success rate: 100%
  • CPU usage: < 20%
  • Memory usage: < 500MB

Scenario 2: Moderate Load (100 req/min)

Objective: Simulate typical production traffic.

Test Script: webhook-moderate.yml

config:
target: "http://localhost:3000"
phases:
- duration: 300 # 5 minutes
arrivalRate: 1.67 # ~100 requests/minute
name: "Moderate load"

scenarios:
- name: "Mixed webhook events"
weight: 40
flow:
- post:
url: "/api/webhooks/stripe"
json:
type: "customer.subscription.created"
id: "evt_{{ $randomString() }}"
data:
object:
id: "sub_{{ $randomString() }}"
customer: "cus_{{ $randomString() }}"
status: "active"

- name: "Payment succeeded"
weight: 30
flow:
- post:
url: "/api/webhooks/stripe"
json:
type: "invoice.payment_succeeded"
id: "evt_{{ $randomString() }}"
data:
object:
id: "in_{{ $randomString() }}"
customer: "cus_{{ $randomString() }}"
amount_paid: 9900

- name: "Payment failed"
weight: 20
flow:
- post:
url: "/api/webhooks/stripe"
json:
type: "invoice.payment_failed"
id: "evt_{{ $randomString() }}"
data:
object:
id: "in_{{ $randomString() }}"
customer: "cus_{{ $randomString() }}"

- name: "Subscription deleted"
weight: 10
flow:
- post:
url: "/api/webhooks/stripe"
json:
type: "customer.subscription.deleted"
id: "evt_{{ $randomString() }}"
data:
object:
id: "sub_{{ $randomString() }}"
customer: "cus_{{ $randomString() }}"

Expected Results:

  • Response time: < 500ms (p95)
  • Success rate: > 99%
  • CPU usage: < 50%
  • Memory usage: < 1GB
  • No database connection errors

Scenario 3: Peak Load (500 req/min)

Objective: Test system limits and identify bottlenecks.

Test Configuration:

config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 2 # Warm up
name: "Warm up"
- duration: 180 # 3 minutes
arrivalRate: 8.33 # ~500 requests/minute
name: "Peak load"
- duration: 60
arrivalRate: 2 # Cool down
name: "Cool down"

# Same scenarios as moderate load

Expected Results:

  • Response time: < 1000ms (p95)
  • Success rate: > 95%
  • CPU usage: < 80%
  • Memory usage: < 2GB
  • Possible rate limiting (429 responses)

Scenario 4: Spike Test

Objective: Test resilience to sudden traffic spike.

Test Configuration:

config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 1 # Normal
name: "Before spike"
- duration: 60
arrivalRate: 20 # Sudden spike (1200 req/min)
name: "Spike"
- duration: 60
arrivalRate: 1 # Recovery
name: "After spike"

Expected Results:

  • System doesn't crash
  • Some requests may fail during spike (acceptable)
  • Recovery after spike within 30 seconds
  • No permanent degradation

Scenario 5: Duplicate Webhook Test

Objective: Verify idempotent webhook processing.

Test Script: webhook-duplicate.js (k6)

import http from 'k6/http';
import { check } from 'k6';

export const options = {
vus: 10, // 10 concurrent users
duration: '60s',
};

const EVENT_ID = 'evt_test_duplicate_12345';

export default function () {
const url = 'http://localhost:3000/api/webhooks/stripe';

const payload = JSON.stringify({
id: EVENT_ID, // Same event ID
type: 'customer.subscription.created',
data: {
object: {
id: 'sub_test',
customer: 'cus_test',
status: 'active'
}
}
});

const params = {
headers: {
'Content-Type': 'application/json',
'stripe-signature': 'dummy'
},
};

const res = http.post(url, payload, params);

check(res, {
'status is 200': (r) => r.status === 200,
});
}

Run Test:

k6 run webhook-duplicate.js

Verify:

// In MongoDB
use automation_platform

// Should only be ONE entry for this event ID
db.webhook_logs.countDocuments({eventId: 'evt_test_duplicate_12345'})
// Expected: 1 (not 600, even though we sent 600 requests)

Expected Results:

  • All requests return 200 OK
  • Only one webhook log entry created
  • No duplicate processing
  • No database errors (unique index works)

Scenario 6: Slow Database Test

Objective: Test webhook handling when database is slow.

Simulate Slow DB:

// In MongoDB, enable profiling
use automation_platform
db.setProfilingLevel(2) // Log all operations

// Add artificial delay (for testing only!)
db.webhook_logs.createIndex(
{ eventId: 1 },
{ unique: true, name: 'idx_slow', commitDelay: 1000 } // 1 second delay
)

Test Configuration:

config:
target: "http://localhost:3000"
phases:
- duration: 120
arrivalRate: 5 # 300 req/min
name: "Slow database"

# Set timeout expectations
timeout: 5 # 5 second timeout

Expected Results:

  • Response times increase (expected)
  • Some timeouts (< 10%)
  • No crashes
  • Queue builds up but processes eventually
  • System recovers when DB returns to normal speed

Cleanup:

// Remove slow index
db.webhook_logs.dropIndex('idx_slow')
db.setProfilingLevel(0)

Performance Metrics

Response Time Targets

MetricTargetCritical
p50 (median)< 200ms< 500ms
p95< 500ms< 1000ms
p99< 1000ms< 2000ms

Throughput Targets

Load LevelRequests/minSuccess Rate
Normal10099.9%
Peak50099%
Spike100095%

Resource Usage Targets

ResourceNormalPeakCritical
CPU< 30%< 70%< 90%
Memory< 500MB< 1.5GB< 3GB
Disk I/O< 50MB/s< 200MB/s< 500MB/s

Monitoring During Tests

Real-Time Metrics

Producer Service Logs:

# Watch for errors
docker-compose logs -f producer | grep -E "ERROR|WARN"

# Count webhook processing
docker-compose logs producer | grep "Webhook verified" | wc -l

# Average processing time (approximate)
docker-compose logs producer | grep "Webhook verified" | tail -100

MongoDB Metrics:

// In mongosh
use automation_platform

// Current operations
db.currentOp()

// Slow queries
db.system.profile.find().sort({millis: -1}).limit(10)

// Connection count
db.serverStatus().connections

// Operation counters
db.serverStatus().opcounters

Docker Stats:

# Live resource usage
docker stats

# Watch specific service
docker stats automation-producer

Post-Test Analysis

Webhook Processing Summary:

// Total webhooks processed
db.webhook_logs.countDocuments()

// By event type
db.webhook_logs.aggregate([
{ $group: { _id: '$eventType', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
])

// Success vs error rate
db.webhook_logs.aggregate([
{ $group: { _id: '$status', count: { $sum: 1 } } }
])

// Processing time distribution
db.webhook_logs.aggregate([
{
$project: {
processingTime: {
$subtract: ['$processedAt', '$createdAt']
}
}
},
{
$bucket: {
groupBy: '$processingTime',
boundaries: [0, 100, 250, 500, 1000, 2000, 5000],
default: 'over 5s',
output: { count: { $sum: 1 } }
}
}
])

Database Performance:

// Index usage
db.webhook_logs.aggregate([
{ $indexStats: {} }
])

// Collection stats
db.webhook_logs.stats()

// Slow queries
db.getProfilingStatus()
db.system.profile.find({ millis: { $gt: 1000 } })

Optimization Recommendations

If Response Times Too Slow (> 1s p95)

1. Add Database Indexes

// Check existing indexes
db.webhook_logs.getIndexes()

// Add missing indexes
db.webhook_logs.createIndex({ organizationId: 1, processedAt: -1 })
db.webhook_logs.createIndex({ status: 1, processedAt: -1 })

2. Optimize Queries

// Use projection to limit fields
db.webhook_logs.find(
{ eventType: 'customer.subscription.created' },
{ eventId: 1, status: 1 } // Only return needed fields
)

// Use explain() to analyze queries
db.webhook_logs.find({ organizationId: 'xxx' }).explain('executionStats')

3. Enable Connection Pooling

// In server.ts
const mongoClient = new MongoClient(MONGO_URI, {
maxPoolSize: 50, // Increase pool size
minPoolSize: 10,
maxIdleTimeMS: 30000
});

If CPU Usage Too High (> 80%)

1. Scale Horizontally

# Add more producer instances
docker-compose up -d --scale producer=3

# Use load balancer (Nginx)

2. Optimize Webhook Processing

// Move heavy operations to background queue
// Quick acknowledge, async processing

app.post('/api/webhooks/stripe', async (request, reply) => {
// Verify and acknowledge immediately
const event = verifyWebhook(request);
reply.send({ received: true });

// Process in background
await webhookQueue.push(event);
});

If Memory Usage Too High (> 2GB)

1. Limit Webhook Payload Storage

// Don't store full payload, only essential fields
await webhookLogsCollection.insertOne({
eventId: event.id,
eventType: event.type,
organizationId,
status: 'success',
// payload: event.data.object, // REMOVE (can be large)
essentials: { // Store only what's needed
subscriptionId: event.data.object.id,
status: event.data.object.status
}
});

2. Cleanup Old Logs

// TTL index already set to 90 days
// Verify it's working
db.webhook_logs.find({
processedAt: { $lt: new Date(Date.now() - 90*24*60*60*1000) }
}).count()
// Should be 0 (old logs auto-deleted)

If Database Connection Errors

1. Increase Connection Pool

const mongoClient = new MongoClient(MONGO_URI, {
maxPoolSize: 100, // Increase from default 50
waitQueueTimeoutMS: 5000
});

2. Implement Connection Retry

async function connectWithRetry(maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
await mongoClient.connect();
return;
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}

Load Test Execution Checklist

Pre-Test

  • All services running
  • Monitoring setup
  • Database backups created
  • Baseline metrics recorded
  • Team notified (if testing production)

During Test

  • Monitor logs for errors
  • Watch resource usage
  • Track response times
  • Note any anomalies
  • Be ready to stop test if critical issues

Post-Test

  • Collect all metrics
  • Analyze results
  • Compare to targets
  • Document findings
  • Create optimization plan
  • Clean up test data

Sample Load Test Report

# Webhook Load Test Report

**Date:** 2026-02-06
**Environment:** Local Development
**Test Duration:** 10 minutes
**Total Requests:** 1,000

## Results Summary

### Response Times
- p50: 185ms ✅ (target: < 200ms)
- p95: 420ms ✅ (target: < 500ms)
- p99: 850ms ✅ (target: < 1000ms)

### Success Rate
- Success: 998 (99.8%) ✅ (target: > 99%)
- Failed: 2 (0.2%)
- Errors: Connection timeout (2)

### Resource Usage
- CPU Average: 35% ✅ (target: < 50%)
- CPU Peak: 62%
- Memory Average: 680MB ✅ (target: < 1GB)
- Memory Peak: 820MB

### Database Performance
- Total operations: 1,000
- Average query time: 45ms
- Slow queries (>1s): 0 ✅
- Connection errors: 0 ✅

## Issues Found
1. **Connection Timeouts (2):** During peak load spike
- Root cause: Connection pool exhausted
- Fix: Increase maxPoolSize to 100

2. **Memory Growth:** Gradual increase over 10 minutes
- From: 400MB → 820MB
- Concern: May leak in long-running tests
- Action: Monitor in production

## Recommendations
1. ✅ System ready for production
2. Increase connection pool size
3. Add memory monitoring alerts
4. Retest after optimizations

## Next Steps
1. Deploy optimizations
2. Run 24-hour soak test
3. Monitor first week in production
4. Review and adjust limits

**Test conducted by:** [Name]
**Reviewed by:** [Name]
**Approved for production:** [Yes/No]

  • docs/testing/billing-test-scenarios.md - Functional test scenarios
  • docs/testing/billing-edge-cases.md - Edge case handling
  • docs/deployment/stripe-production-checklist.md - Production deployment
  • docs/implementation/phase-3/webhook-testing-guide.md - Webhook testing

Additional Resources

Load Testing Tools:

Stripe Performance:

MongoDB Performance: