Event-Driven APIs: Webhooks, WebSockets, SSE, and Async Patterns
Event-Driven APIs: Webhooks, WebSockets, SSE, and Async Patterns
REST APIs are request-response: the client asks, the server answers. But many real-world use cases need the server to push data to the client — payment confirmations, live chat, stock prices, build status updates. Event-driven APIs flip the model: the server notifies the client when something happens.
The Four Patterns
| Pattern | Direction | Connection | Best For |
|---|---|---|---|
| Webhooks | Server → Server | HTTP callback | Backend notifications, integrations |
| WebSockets | Bidirectional | Persistent TCP | Chat, gaming, collaborative editing |
| SSE | Server → Client | Persistent HTTP | Live feeds, dashboards, notifications |
| Async Request-Reply | Client → Server → Client | Polling or callback | Long-running operations |
1. Webhooks
How They Work
Your API sends an HTTP POST to the customer's URL when an event occurs:
Event occurs (e.g., payment succeeds)
→ Your API sends POST to customer's webhook URL
→ Customer's server processes the event
→ Customer returns 200 OK (acknowledgment)
Implementation
Provider side (sending webhooks):
1. Customer registers a webhook URL: https://their-app.com/webhooks
2. Event occurs in your system
3. Build webhook payload with event data
4. Sign the payload (HMAC-SHA256)
5. POST to customer's URL with signature header
6. If non-2xx response: retry with exponential backoff
7. After N failures: disable webhook, notify customer
Payload format:
{
"id": "evt_a1b2c3d4",
"type": "payment.succeeded",
"created": "2026-03-08T14:30:00Z",
"data": {
"payment_id": "pay_xyz",
"amount": 9900,
"currency": "usd"
}
}
Security
Sign every webhook so consumers can verify it came from you:
Signature = HMAC-SHA256(webhook_secret, raw_request_body)
Header: X-Webhook-Signature: sha256=a1b2c3d4...
Consumer verifies:
expected = HMAC-SHA256(their_webhook_secret, raw_body)
if (expected !== received_signature) reject
Additional security measures:
- Include a timestamp to prevent replay attacks
- Allow customers to rotate webhook secrets
- Use HTTPS only (never send webhooks over HTTP)
- Include an idempotency key so consumers can deduplicate
Retry Strategy
Attempt 1: Immediate
Attempt 2: 1 minute later
Attempt 3: 5 minutes later
Attempt 4: 30 minutes later
Attempt 5: 2 hours later
Attempt 6: 8 hours later
Attempt 7: 24 hours later → Give up, mark webhook as failing
After 3 consecutive days of failures: Disable the webhook and email the customer.
When to Use Webhooks
✅ Payment notifications, order updates, CI/CD build results, CRM events ❌ Real-time chat, live data streams, interactive experiences
2. WebSockets
How They Work
Persistent bidirectional connection over TCP:
Client: GET /ws (Upgrade: websocket)
Server: 101 Switching Protocols
── Connection established ──
Client → Server: { "type": "subscribe", "channel": "chat-123" }
Server → Client: { "type": "message", "text": "Hello!" }
Client → Server: { "type": "message", "text": "Hi back!" }
Server → Client: { "type": "message", "text": "How are you?" }
── Connection persists until explicitly closed ──
Implementation Considerations
Connection management:
| Concern | Solution |
|---|---|
| Connection drops | Automatic reconnect with backoff |
| Authentication | Auth on initial handshake (token in query param or first message) |
| Heartbeat | Ping/pong every 30 seconds to detect dead connections |
| Scaling | Sticky sessions or pub/sub backplane (Redis) |
| Load balancing | Layer 7 LB with WebSocket support (nginx, HAProxy) |
Message format:
{
"type": "event_type",
"id": "msg_123",
"timestamp": "2026-03-08T14:30:00Z",
"data": { ... }
}
Scaling WebSockets
WebSocket connections are stateful — each connection lives on a specific server. Scaling requires:
Clients ↔ Load Balancer (sticky sessions)
↓
Server 1 Server 2 Server 3
↓ ↓ ↓
└─── Redis Pub/Sub ──┘
(broadcast messages across servers)
Connection limits:
| Scale | Connections per Server | Servers Needed |
|---|---|---|
| Small | 10K | 1 |
| Medium | 50K | 2-5 |
| Large | 100K+ | 10+ with auto-scaling |
When to Use WebSockets
✅ Chat, gaming, collaborative editing, live trading, real-time multiplayer ❌ One-directional server pushes, infrequent updates, server-to-server
3. Server-Sent Events (SSE)
How They Work
One-directional stream from server to client over HTTP:
Client: GET /events (Accept: text/event-stream)
Server: 200 OK (Content-Type: text/event-stream)
── Stream opens ──
data: {"type": "update", "price": 150.25}
data: {"type": "update", "price": 150.30}
data: {"type": "update", "price": 149.95}
── Stream stays open indefinitely ──
SSE Format
event: price-update
id: 42
retry: 3000
data: {"symbol": "AAPL", "price": 150.25}
event: news
id: 43
data: {"headline": "Market opens higher"}
| Field | Purpose |
|---|---|
event | Event type (for client-side filtering) |
id | Last event ID (for reconnection — "give me events after 43") |
retry | Reconnection delay in milliseconds |
data | Event payload (can be multi-line) |
SSE vs. WebSocket
| Dimension | SSE | WebSocket |
|---|---|---|
| Direction | Server → Client only | Bidirectional |
| Protocol | HTTP | WebSocket (TCP) |
| Reconnection | Automatic (built into spec) | Manual implementation |
| Data format | Text only | Text or binary |
| Browser support | All modern browsers | All modern browsers |
| Through proxies | Works (it's HTTP) | Sometimes blocked |
| Scaling | Stateless-friendly | Requires sticky sessions |
| Max connections | 6 per domain (HTTP/1.1), unlimited (HTTP/2) | No limit |
Use SSE when you only need server-to-client pushes. It's simpler, more reliable, and works better with existing HTTP infrastructure.
When to Use SSE
✅ Live dashboards, notification feeds, AI streaming responses, build logs, stock tickers ❌ Chat (need bidirectional), binary data, gaming
4. Async Request-Reply
How It Works
For long-running operations that can't return immediately:
Client: POST /api/reports/generate
Server: 202 Accepted
{ "job_id": "job_123", "status_url": "/api/jobs/job_123" }
── Client polls status ──
Client: GET /api/jobs/job_123
Server: { "status": "processing", "progress": 45 }
Client: GET /api/jobs/job_123
Server: { "status": "completed", "result_url": "/api/reports/abc.pdf" }
Implementation Patterns
Pattern 1: Polling
1. Client submits request → 202 Accepted with job ID
2. Client polls status endpoint every N seconds
3. Server returns progress updates
4. When complete: server returns result or download URL
Pattern 2: Webhook Callback
1. Client submits request with callback URL → 202 Accepted
2. Server processes asynchronously
3. Server POSTs result to callback URL when done
4. No polling needed
Pattern 3: WebSocket Notification
1. Client connects WebSocket and submits request
2. Server processes asynchronously
3. Server pushes progress updates and final result via WebSocket
Response Codes
| Code | Meaning | Use |
|---|---|---|
202 Accepted | Request accepted for processing | Initial submission |
200 OK with status | Job still processing | Status poll |
303 See Other | Job complete, redirect to result | Completion (with Location header) |
When to Use Async Request-Reply
✅ Report generation, video processing, data imports, ML inference, batch operations ❌ Simple CRUD, instant responses, real-time interaction
Choosing the Right Pattern
Does the server need to notify the client?
├── No → Standard REST (request-response)
└── Yes
├── Server-to-server notification?
│ └── Webhooks
├── Client needs live updates?
│ ├── Bidirectional? → WebSocket
│ └── Server-to-client only? → SSE
└── Long-running operation?
└── Async Request-Reply
Pattern Comparison Summary
| Criteria | Webhooks | WebSocket | SSE | Async Reply |
|---|---|---|---|---|
| Complexity | Medium | High | Low | Medium |
| Infrastructure | Simple | Complex (stateful) | Simple | Medium |
| Scaling difficulty | Low | High | Medium | Low |
| Latency | Seconds | Milliseconds | Milliseconds | Seconds-minutes |
| Reliability | Retry-based | Connection-dependent | Auto-reconnect | Polling-based |
| Firewall-friendly | Yes | Sometimes no | Yes | Yes |
Hybrid Architecture
Most production systems combine multiple patterns:
Payment API:
- REST for creating charges (sync)
- Webhooks for payment confirmations (async server-to-server)
- SSE for dashboard live updates (async server-to-client)
Collaboration App:
- REST for CRUD operations (sync)
- WebSocket for real-time editing (bidirectional)
- SSE for presence indicators (server-to-client)
- Webhooks for third-party integrations (server-to-server)
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Using WebSocket when SSE suffices | Over-engineered, harder to scale | SSE for server-push only |
| Polling instead of webhooks | Wasted resources, higher latency | Implement webhooks |
| No webhook retry logic | Lost events on temporary failures | Exponential backoff + DLQ |
| WebSocket without heartbeat | Zombie connections consume resources | Ping/pong every 30 seconds |
| No reconnection logic | Dropped connections stay dropped | Auto-reconnect with backoff |
| Unsigned webhooks | Security vulnerability | HMAC-SHA256 signatures |
Building event-driven APIs? Explore async API patterns and tools on APIScout — comparisons, guides, and developer resources.