Webhook Dispatcher (Rust)
I kept running into the same problem: teams need reliable webhooks, but the choices are either build a full system or outsource it. This crate is the middle path. It gives you a production-ready webhook dispatcher that lives inside your app.
It focuses on the parts that actually hurt in real systems: fairness, retries, DLQ, signatures, rate limits, and durability. No hosted service required.
Quickstart
use ;
async
Installation
Add to your Cargo.toml:
[]
= "0.1"
Enable optional features if needed:
[]
= { = "0.1", = ["http", "redis", "postgres", "metrics", "tracing"] }
Concepts
- Endpoint: where a webhook goes and how it behaves.
- Event: the data you want to send.
- Dispatcher: the engine that schedules and delivers.
- DLQ: where failures land so you can replay them.
API Guide
- Make a dispatcher (in-memory or durable).
- Register endpoints (where to send).
- Dispatch events (what to send).
- Verify on the receiver.
- Check status or replay from DLQ if needed.
Production Checklist
- Enable real HTTP delivery:
--features http - Use a durable backend: Redis or Postgres
- Choose an overflow policy:
BlockorSpillToStorage - Set rate limits (endpoint + tenant)
- Verify signatures on the receiver side
Receiver Verification
use verify_webhook_request;
let headers = vec!;
verify_webhook_request?;
Features
- Fair scheduling with sharded queues
- Retries with jitter + DLQ
- Per-endpoint retry policy overrides
- HMAC signatures and timestamp support
- Per-endpoint rate limiting
- Multi-tenant isolation
- Pluggable storage (in-memory, Redis, Postgres)
- Metrics and tracing feature flags
Optional Features
Durable Storage Backends
In-Memory (default)
Fast and simple, but not durable across restarts.
let dispatcher = new;
Redis
use Arc;
use ;
let client = open?;
let storage = new;
let dispatcher = new_with_storage.await;
Postgres
use Arc;
use ;
let =
connect.await?;
spawn;
let storage = new;
let dispatcher = new_with_storage.await;
Common Recipes
Set Overflow Policy
use ;
let mut cfg = default;
cfg.overflow_policy = Block;
Per-Endpoint Retry Overrides
use Endpoint;
let endpoint = new
.with_retry_policy;
Tenant Rate Limits
use ;
dispatcher
.set_tenant_rate_limit
.await;
DLQ Replay
use IdempotencyKey;
let replayed = dispatcher.replay_dlq_all.await;
let ok = dispatcher.replay_dlq_entry.await;
Delivery Status Queries
use IdempotencyKey;
let status = dispatcher.delivery_status.await;
Usage Notes
- This is a library. You call it from your app.
- For real delivery, enable
httpand point to a real URL.
Example Configs (Different Scales)
Small (dev / side-project)
use DispatcherConfig;
let cfg = DispatcherConfig ;
Medium (startup)
use DispatcherConfig;
let cfg = DispatcherConfig ;
Large (high throughput)
use DispatcherConfig;
let cfg = DispatcherConfig ;
Architecture Diagram
┌───────────────┐
│ Dispatcher │
└──────┬────────┘
│ dispatch(event)
┌──────▼────────┐
│ Sharded Queues│ (fair scheduling)
└──────┬────────┘
│
┌──────▼────────┐
│ Scheduler │ (retry + jitter + DLQ)
└──────┬────────┘
│
┌──────▼────────┐
│ Workers │ (rate limit + HTTP)
└──────┬────────┘
│
┌──────▼────────┐
│ Endpoints │
└───────────────┘
Troubleshooting
I am not seeing webhooks delivered
- Ensure you ran with
--features http. - Check endpoint URL and network access.
- If you see DLQ entries, replay them or check the status.
I am getting backpressure errors
- Increase
shard_queue_sizeormax_in_flight. - Use
OverflowPolicy::Blockfor safer behavior.
Retries do not seem to happen
- Confirm
max_retriesis set on the endpoint. - Verify that the failure is retryable (4xx is non-retryable).
Signature verification fails
- Make sure the secret matches.
- Confirm the timestamp is within
max_age_secs. - Validate header names match your endpoint config.
Redis/Postgres durability not working
- Confirm the feature flag is enabled (
redisorpostgres). - Ensure the storage backend is used via
new_with_storage. - Check connection details and permissions.
Prometheus Metrics Example
Add the exporter in your app (not required by the library):
# Cargo.toml
= "0.15"
use PrometheusBuilder;
let _handle = new.install.unwrap;
Then run with --features metrics and scrape the metrics endpoint exposed by your app.
Notes
- Default mode is in-memory (fast, not durable across restarts).
- Use
new_with_storagefor durable backends.