webhook-dispatcher 0.1.2

In-process webhook delivery engine with retries, DLQ, signatures, rate limits, and pluggable storage.
Documentation

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 webhook_dispatcher::{Dispatcher, DispatcherConfig, Endpoint, Event};

#[tokio::main]
async fn main() {
    let dispatcher = Dispatcher::new(DispatcherConfig::default());

    let endpoint = Endpoint::new("orders", "https://example.com/webhook")
        .with_secret(b"supersecret")
        .with_tenant_id("tenant_a")
        .with_rate_limit(100, 200);

    dispatcher.register_endpoint(endpoint).await;

    let event = Event::new("evt_123", r#"{"id":123}"#.as_bytes())
        .with_tenant_id("tenant_a");
    let _ = dispatcher.dispatch(event, vec!["orders".into()]).await;
}

Installation

Add to your Cargo.toml:

[dependencies]
webhook-dispatcher = "0.1"

Enable optional features if needed:

[dependencies]
webhook-dispatcher = { version = "0.1", features = ["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

  1. Make a dispatcher (in-memory or durable).
  2. Register endpoints (where to send).
  3. Dispatch events (what to send).
  4. Verify on the receiver.
  5. Check status or replay from DLQ if needed.

Production Checklist

  1. Enable real HTTP delivery: --features http
  2. Use a durable backend: Redis or Postgres
  3. Choose an overflow policy: Block or SpillToStorage
  4. Set rate limits (endpoint + tenant)
  5. Verify signatures on the receiver side

Receiver Verification

use webhook_dispatcher::verify_webhook_request;

let headers = vec![
    ("X-Webhook-Signature", "abcd..."),
    ("X-Webhook-Timestamp", "1700000000"),
];

verify_webhook_request(
    headers.iter().map(|(k, v)| (*k, *v)),
    payload_bytes,
    b"supersecret",
    "X-Webhook-Signature",
    "X-Webhook-Timestamp",
    300,
    now_secs,
)?;

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

cargo build --features http
cargo build --features redis
cargo build --features postgres
cargo build --features metrics
cargo build --features tracing

Durable Storage Backends

In-Memory (default)

Fast and simple, but not durable across restarts.

let dispatcher = Dispatcher::new(DispatcherConfig::default());

Redis

use std::sync::Arc;
use webhook_dispatcher::{Dispatcher, DispatcherConfig, RedisStorage};

let client = redis::Client::open("redis://127.0.0.1/")?;
let storage = Arc::new(RedisStorage::new(client, "webhook_dispatcher"));
let dispatcher = Dispatcher::new_with_storage(DispatcherConfig::default(), storage).await;

Postgres

use std::sync::Arc;
use webhook_dispatcher::{Dispatcher, DispatcherConfig, PostgresStorage};

let (client, connection) =
    tokio_postgres::connect("host=localhost user=postgres", tokio_postgres::NoTls).await?;
tokio::spawn(connection);

let storage = Arc::new(PostgresStorage::new(client).await?);
let dispatcher = Dispatcher::new_with_storage(DispatcherConfig::default(), storage).await;

Common Recipes

Set Overflow Policy

use webhook_dispatcher::{DispatcherConfig, OverflowPolicy};

let mut cfg = DispatcherConfig::default();
cfg.overflow_policy = OverflowPolicy::Block;

Per-Endpoint Retry Overrides

use webhook_dispatcher::Endpoint;

let endpoint = Endpoint::new("orders", "https://example.com/webhook")
    .with_retry_policy(200, 2_000, 50);

Tenant Rate Limits

use webhook_dispatcher::{Dispatcher, TenantId};

dispatcher
    .set_tenant_rate_limit(TenantId("tenant_a".to_string()), 300, 600)
    .await;

DLQ Replay

use webhook_dispatcher::IdempotencyKey;

let replayed = dispatcher.replay_dlq_all().await;
let ok = dispatcher.replay_dlq_entry(&IdempotencyKey::new(
    "evt_123".into(),
    "orders".into(),
    Some("tenant_a".into()),
)).await;

Delivery Status Queries

use webhook_dispatcher::IdempotencyKey;

let status = dispatcher.delivery_status(&IdempotencyKey::new(
    "evt_123".into(),
    "orders".into(),
    Some("tenant_a".into()),
)).await;

Usage Notes

  • This is a library. You call it from your app.
  • For real delivery, enable http and point to a real URL.

Example Configs (Different Scales)

Small (dev / side-project)

use webhook_dispatcher::DispatcherConfig;

let cfg = DispatcherConfig {
    max_in_flight: 50,
    shard_queue_size: 200,
    shard_count: 8,
    worker_count: 2,
    ..Default::default()
};

Medium (startup)

use webhook_dispatcher::DispatcherConfig;

let cfg = DispatcherConfig {
    max_in_flight: 500,
    shard_queue_size: 2_000,
    shard_count: 32,
    worker_count: 8,
    ..Default::default()
};

Large (high throughput)

use webhook_dispatcher::DispatcherConfig;

let cfg = DispatcherConfig {
    max_in_flight: 5_000,
    shard_queue_size: 10_000,
    shard_count: 128,
    worker_count: 32,
    ..Default::default()
};

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_size or max_in_flight.
  • Use OverflowPolicy::Block for safer behavior.

Retries do not seem to happen

  • Confirm max_retries is 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 (redis or postgres).
  • 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
metrics-exporter-prometheus = "0.15"
use metrics_exporter_prometheus::PrometheusBuilder;

let _handle = PrometheusBuilder::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_storage for durable backends.