Go Webhook Receiver Starter

A small Go service that receives webhooks safely: verified, deduplicated, stored durably and retried, with your code in one function.

Buy on Polar: $24 One-time purchase · v0.2.0 · instant download via Polar

Go Webhook Receiver Starter cover
Output from a real test run, not a mock-up. Counts differ slightly from run to run, so figures in the text may not match this capture exactly.

What you get

A complete service you can read in an afternoon, not a framework. Your logic goes in one function, handle(); everything around it is already done and tested.

sender --POST--> /webhook/{source}
                   1. read the body (size limit)
                   2. verify the HMAC signature   -> 401 if wrong, nothing stored
                   3. dedupe by delivery id       -> the same event twice is handled once
                   4. insert into SQLite          -> 202 Accepted, fast
worker ----------> handle(event)   retries with backoff, dead after N attempts

Features

  • Three signature schemes. GitHub style, Stripe style (with a replay window) and Standard Webhooks, the scheme Polar uses. Constant-time comparison.
  • Secret rotation. WEBHOOK_SECRET takes a comma-separated list, so old and new secrets are both accepted during a rotation and no events are dropped.
  • Deduplication. By delivery id (the webhook-id header is used automatically for Standard Webhooks) or, if there is none, by a hash of the body. Backed by a uniqueness constraint, so it survives restarts.
  • Durable queue. SQLite in WAL mode; the request is acknowledged after one commit. Exponential backoff, dead-letter after MAX_ATTEMPTS, and recovery of events left in processing by a crash.
  • Concurrency. WORKERS=N workers; an event is never given to two workers.
  • Operations. server replay for dead events, server purge and RETENTION_DAYS for old rows, /metrics in Prometheus format, a JSON access log with request ids that never contains payloads, headers or secrets.
  • Safe handlers. worker.Permanent(err) parks a payload that can never succeed without burning retries. A shutdown mid-handler returns the event to the queue without losing an attempt.
  • Examples. A Polar order.paid handler and a generic JSON router, both with tests. Dockerfile and systemd unit included.

Pure Go (no cgo). Dependencies: the standard library and modernc.org/sqlite.

Measured

Method: the included load tester (make loadtest) against the compiled binary on an Apple Silicon laptop, 32 connections, 512-byte payloads, real HTTP over loopback, default durability (WAL, synchronous=FULL), 5,000 events per run, three runs.

WhatResult
Events accepted over HTTP (verify, durable insert, 202), GitHub styleabout 9,200 to 9,400 per second (p50 2.4 ms, p99 about 15 ms)
Same, Standard Webhooksabout 9,100 to 9,700 per second
End to end, from first request to empty queue (no-op handler), 1 workerabout 3,700 to 3,800 per second
Queue drain, handler that sleeps 5 ms (like a network call), 1 / 8 / 32 workersabout 160 / 1,460 / 5,500 per second

How to read it: the database is the bottleneck, not HTTP. All SQLite access goes through one connection, so with a no-op handler extra workers do not help; they pay off when your handler waits on something. Not measured: Linux or cloud VMs and network disks, other payload sizes, millions of rows, TLS or a proxy, memory and CPU, long soak runs. Treat the numbers as "this machine, this setup".

What was tested, and what was not

make test runs go vet and 41 tests with the race detector. They cover tampered, stale, future and malformed signatures, the Standard Webhooks known-answer vector from the reference Go library, secret rotation, 401 storing nothing, duplicate deliveries, oversize and wrong-method requests, retries and backoff, dead-letter, crash recovery, ordering, many workers never processing an event twice, replay, purge and upgrading a 0.1.0 database. The compiled binary, built from the packaged zip, was also exercised with real signed HTTP requests for all three schemes, including a replay of the same delivery.

Not tested:

  • A real Polar delivery. The Polar example uses field names from Polar's public documentation and API types, and the handling of older Polar secrets (raw: prefix) follows what their docs say. Check one real event before relying on either.
  • The Docker image build and the systemd unit
  • Linux or cloud disks, large tables, behaviour behind a proxy or TLS, long soak runs
  • -race on Go 1.22 (the suite passes on 1.22 without -race, and with -race on a newer toolchain)

Who it is for, and who it is not

For developers who need to receive webhooks from Polar, Stripe, GitHub or similar and want the unglamorous parts done right: signatures, retries, dedupe and a queue that survives a crash.

Not for: workloads that need a distributed queue or exactly-once processing across machines. This is a single process with a single SQLite writer, and events are processed in order by default. At-least-once is the honest guarantee, so your handler must be idempotent.

FAQ

Does it verify Polar webhooks?

Yes, Polar uses Standard Webhooks, which is supported. I could not receive a live Polar delivery, so test with one real event.

Can I use only the signature code?

Yes. It is available separately as a small MIT-licensed module, webhooksig; see the free tools page.

Do I need CGO or a C compiler?

No. The SQLite driver is pure Go.

Which Go version?

Go 1.22 or newer.

What is the license?

Use it in your own projects. Do not resell the source as a competing starter.

Changelog

  • 0.2.0: Standard Webhooks, secret rotation, multiple workers, replay and purge commands, /metrics, access log, Polar example, measured numbers.
  • 0.1.0: first release.

Read more

Get it

Buy on Polar: $24

Payment, tax and invoices are handled by Polar as merchant of record. You receive a download link after checkout.