+43%sustained alert-ingest throughput
2.4×better p99 latency under the same load
~10×smaller idle memory footprint

Why rewrite a backend that works

The original OpsPing backend was TypeScript on Node, and we don't regret it — a small team with a deadline should pick the productive, boring stack. It got the product built. But a paging backend has an unforgiving job description: accept alerts from every webhook and agent pointed at it, write every one of them down, and stay responsive at 3 AM while everything around it is on fire. That job is concurrency, I/O, and JSON — and under sustained concurrent load, the runtime's cut of every request stopped being negligible.

So we rewrote it in Go. Same routes, same JSON payloads, same auth, same OpsGenie-compatible API surface — nothing that speaks to OpsPing's API can tell the two implementations apart. We swapped the engine and kept the car. What follows is the honest accounting of what that bought, measured rather than felt.

Twelve-year-old silicon, on purpose

The benchmark machine for both versions is a 2013-era Intel Xeon E5-2660 v2. That's a deliberate choice, not a budget constraint. Modern hardware is very forgiving of inefficient code — enough cores and cache will make almost anything look acceptable. Old silicon has no such manners. Running the benchmarks on a 2013 Xeon stresses the code instead of the CPU's marketing budget.

It's also the honest ceiling check: if the backend is fast on 2013 hardware, it'll be fast on anything you're likely to run — including the modest box a private single-tenant deployment might live on. Both versions got identical conditions: same machine, same Postgres, identical load, side by side.

The numbers

Every number below follows the same protocol: identical hardware, identical database, identical load. Ingest throughput is counted from database rows — what actually landed in Postgres — not from what the load tool claimed it sent. Client-side counters drift in exactly the direction you'd hope.

The memory figures are production numbers, not benchmark numbers: idle RSS of the live process. The login path is bcrypt-bound — deliberately slow by design — which makes it a clean CPU-bound concurrency benchmark. The endpoint gains come from the framework and JSON layers alone.

Thirty minutes, flat out

Sustained load tells the truth that burst benchmarks skip. We ran the Go backend at a flat 400/s ingest for 30 minutes straight: 728,659 alerts written, zero server errors. The limiting factor was our load generator, which ran out of headroom before the server did. When the bottleneck is the tool generating the load, the capacity story is going well.

How we made the swap boring

A rewrite is only as good as its cutover. Ours was engineered to be an event nobody noticed:

Methodology, in short. Identical hardware (2013-era Intel Xeon E5-2660 v2), identical Postgres, identical load — TypeScript and Go A/B. Load generated with k6 plus a custom load driver for the ingest path. Ingest counts are ground truth from database rows, not client-side counters.

What's next

We didn't do this to win a benchmark. We did it because the tail latency and the memory bill were the two lines we didn't like on our own graphs — the numbers above are the receipt.

Practically, a ~15 MB static binary that idles at ~17 MB RSS runs comfortably on small, cheap machines. That's good for our costs and better for anyone running OpsPing as a private single-tenant deployment on hardware they already own. Next up is more of what made this rewrite safe: identical APIs, boring migrations, and benchmarks we'd be glad to see someone else reproduce.

— The OpsPing team

Migration guide Changelog Launch the App