HookRelay is a lightweight, concurrent webhook delivery broker written in Go.
I built this project to get hands-on experience with concurrent programming and distributed system patterns. Sending a webhook is easy, but reliably delivering it when the downstream server is slow, rate-limiting you, or completely dead is an engineering challenge.
Instead of just building a standard CRUD app, I wanted to tackle some real-world reliability problems:
- Circuit Breaker: I implemented a custom circuit breaker with a background GC. It prevents the broker from hammering dead endpoints and transitions to a Half-Open state to test recovery.
- Smart Retries & Min-Heap: Implemented a custom min-heap to efficiently schedule retries
O(log n). It uses exponential backoff with jitter and strictly honors the upstreamRetry-AfterHTTP headers. - Concurrency: The core runs on a non-blocking worker pool (Goroutines & Channels) with atomic counters, making it safe and fast.
- Security: Outbound payloads are signed using HMAC SHA-256 (
X-Hook-Signature-256), and the control API is protected by a static Bearer token.
Here is how a webhook flows through the app:
- Inbound: When an event hits the API, it's shoved directly into a buffered
jobschannel so the HTTP handler can return a202 Acceptedimmediately. - Delivery (Worker Pool): A number of dedicated goroutines constantly listen to
jobschannel and attempt to send the actual HTTP requests. - Circuit Breaker: Before making the request, the worker checks with CB whether the URL is healthy. If the target server is known to be dead, we abort early.
- Retry Loop: If the delivery fails, the job gets passed to the Retry Scheduler and it is placed in a min-heap. Once the exponential backoff timer expires, it is pushed back into the
jobschannel for another attempt.
You'll need Go 1.27.1+ or Docker.
git clone https://github.com/yourusername/hookrelay.git
cd hookrelay
go run ./cmd/hookrelayYou can easily run the project using Docker Compose.
docker compose up -dThe server will start on the defined port (defaults to 8080).
You can configure the broker via .env or environment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
HTTP server port |
API_KEY |
(empty) | Secret key for Bearer auth (auth disabled if empty) |
WORKER_COUNT |
8 |
Number of concurrent delivery workers |
QUEUE_SIZE |
1024 |
Size of the main delivery channel buffer |
MAX_RETRIES |
13 |
Maximum number of retry attempts per webhook |
BASE_BACKOFF |
1s |
Initial delay for exponential backoff |
MAX_BACKOFF |
1h |
Maximum delay cap for exponential backoff |
FAILURES_THRESHOLD |
5 |
Failures needed to trip the Circuit Breaker |
Provide your API key via Authorization: Bearer <API_KEY>.
POST /v1/subscribe- Register a new URL for a topic.{ "topic": "chat", "target_url": "https://api.example.com/webhook", "secret": "optional-hmac-secret" }GET /v1/subscribers?topic=chat- List active subscribers.DELETE /v1/subscribe?topic=chat&target_url=https://api.example.com/webhook- Remove a subscriber.
POST /v1/publish- Broadcast an event to all subscribers of a topic.{ "topic": "chat", "payload": { "username": "miguel1222", "message": "hello!" } }
GET /v1/stats- Internal metrics (successes, failures, dropped events, queue size).GET /v1/health- Basic liveness probe.GET /v1/ready- Readiness probe (returns 200 OK only when workers are running).
- Replace in-memory maps with a durable datastore (SQLite or PostgreSQL) to survive restarts.
- Introduce a dead letter queue to log permanantly failed webhooks.