Skip to content

Latest commit

 

History

History
158 lines (97 loc) · 16 KB

File metadata and controls

158 lines (97 loc) · 16 KB

Ding Benchmark Report

Run date: 2026-03-23 Platform: Apple M3, Darwin arm64 Go version: go1.26.1 Methodology: All benchmarks run from source against a locally-built ding binary. Go engine benchmarks use go test -bench -benchtime=5s. End-to-end benchmarks (latency, throughput, startup) are shell-driven with real HTTP traffic. Prometheus figures are a mix of directly measured (startup, config lines) and published reference values (latency, throughput). Full scripts and raw results are in benchmarks/.


Summary

Metric Ding Prometheus (default) Difference
Alert latency p50 4 ms 62,000 ms 15,500× faster
Alert latency p99 16 ms 91,000 ms 5,687× faster
HTTP ingestion throughput 116,085 rps ~78,000 rps (reference) 49% higher
Cold-start time p50 9 ms 185 ms 20× faster
Cold-start time p99 14 ms 277 ms 20× faster
Config lines (minimal working config) 12 lines, 1 file 30 lines, 3 files 2.5× less

Finding 1: Alert Latency — 4ms Median, 15,500× Faster Than Prometheus Default

The numbers: Ding fires a webhook in a median of 4ms and a p99 of 16ms after receiving an event that crosses a threshold. Prometheus with its default scrape and evaluation configuration has a theoretical minimum latency of roughly 62 seconds (p50) to 91 seconds (p99). That is not a slow run — those numbers are derived from the configuration itself: a 15-second scrape interval, a 15-second evaluation interval, and a 30-second group_wait before Alertmanager dispatches.

Why this matters: For most alerting scenarios the Prometheus default latency is acceptable. But there is a meaningful class of situations where it is not. A payment processor that begins dropping transactions wants to know within seconds, not within a minute. A security system detecting an anomalous login pattern has a narrow window to act before damage is done. A CI pipeline that blows past a memory limit should alert before the build runner OOMs and loses its work.

More broadly, the gap reveals something architectural. Prometheus is built around a pull model: it scrapes targets on a schedule. No matter how fast your infrastructure is, you cannot get an alert faster than one scrape interval plus one evaluation cycle plus the Alertmanager dispatch delay. That lower bound is baked in by design. Ding is built around a push model: the event is evaluated the moment it arrives. The latency is bounded only by network and CPU, not by polling intervals.

The 4ms p50 is not a claim of superiority over all observability platforms — Prometheus's latency is appropriate for its purpose. It is a claim about fit for purpose. For applications that need sub-second alerting, Ding's architecture is the right one.

Benchmark methodology note: The 100-sample latency test sends a JSON event to POST /ingest, then polls a webhook log for the receipt timestamp. The difference is the wall-clock latency from send to receipt. The Prometheus figure is a theoretical minimum computed from the documented default configuration, cross-checked with a 5-run manual test.


Finding 2: Ingestion Throughput — 116,085 Requests per Second

The numbers: Ding's HTTP ingest endpoint sustained 116,085 requests per second under 50 concurrent connections over a 30-second window, with a p99 response latency under 1ms. The Prometheus remote_write receiver, which is the comparable push-based ingest path, is benchmarked by the Prometheus project at roughly 78,000 samples/second in protobuf format.

Why this matters: Throughput is not usually the bottleneck in alerting. The question is whether the tool can keep up with the metric emission rate of the application it is watching. In most cases, applications emit metrics in the hundreds or low thousands per second, and any modern tool handles that comfortably.

The figure becomes relevant at the extremes: high-frequency financial data, log-derived metrics from a busy web service, or IoT sensor arrays where a single Ding instance aggregates hundreds of device streams. At 116k rps, Ding has significant headroom. A web server handling 10,000 requests per minute and emitting one metric per request sends roughly 167 events per second — Ding handles that with 0.14% of its capacity.

The architectural reason Ding is fast here is the same reason it is fast on latency: there is no storage layer. Receiving an event, evaluating it against rules, and updating ring buffers is a pure-CPU operation. There is no disk I/O, no WAL write, no index update. The bottleneck is the evaluation logic itself, not the persistence layer.

Benchmark methodology note: Throughput was measured using hey with 50 concurrent workers over 30 seconds sending valid JSON payloads to /ingest. The Prometheus figure is a published reference value from the Prometheus team's remote_write benchmark suite and is included for directional comparison, not as a directly measured head-to-head.


Finding 3: Engine Performance — 106 ns Per Event, 6 Million Events Per Second Per Core

The numbers: Go microbenchmarks measure the evaluation engine in isolation, without network overhead:

Benchmark Time per operation Allocations
Simple threshold rule (value > 95) 106 ns 2 allocs
Windowed aggregation (avg(value) over 5m > 80, 1,000-entry warm buffer) 157 ns 3 allocs
100 windowed rules, one event 16,580 ns (166 ns/rule) 300 allocs
Engine init (100 rules) 32,011 ns 206 allocs
Engine reinit (hot-reload, 100 rules) 32,295 ns 206 allocs

Why the simple rule number matters: 106 nanoseconds per event means the evaluation loop itself can process roughly 9.4 million events per second on a single core. The HTTP server and network are the practical ceiling, not the rule evaluator. This tells you that rule evaluation is never the bottleneck — the overhead of running a rule is negligible compared to any I/O the application does to emit the metric in the first place.

Why the windowed rule number matters: Windowed aggregation (avg(value) over 5m) is algorithmically more expensive than a simple threshold — it requires maintaining a ring buffer and scanning it on every evaluation. The measured overhead is 157ns, just 48% more than the simple case. That is a remarkably small penalty. The reason is that ring buffers in memory have excellent cache locality, and the scan is a tight loop over a contiguous slice of floats. There is no index lookup, no B-tree traversal, no disk seek. The window is computed fresh each time from raw values, which sounds expensive but is cheap because memory is fast.

Why the 100-rule number matters: At 100 windowed rules, the cost is 16,580ns per event — effectively 166ns per rule. The scaling is linear and predictable. A deployment with 10 rules costs roughly 1,660ns per event; 50 rules costs roughly 8,300ns. Users can add rules without worrying about quadratic blowup or hidden overhead.

Why the reinit number matters: Config hot-reloads are a normal operational event — someone changes a threshold and sends SIGHUP. The reinit benchmark measures this path: building a new engine from 100 rules while the old one continues processing. At 32 microseconds, a hot-reload is instantaneous from the user's perspective. There is no pause, no dropped events, no service restart. The swap happens atomically behind a mutex.


Finding 4: Startup Time — 9ms Cold Start, 20× Faster Than Prometheus

The numbers: Ding's median time from process launch to first healthy HTTP response is 9ms (p99: 14ms). Prometheus's cold start — pulling the Docker image (already cached), initializing TSDB, loading configuration — has a median of 185ms (p99: 277ms).

Why this matters at face value: A 9ms startup means that in environments where Ding is started on demand — a CI pipeline invocation, a container that scales from zero, a systemd service being restarted after a crash — it is ready almost immediately. The startup cost is invisible.

Why this matters architecturally: Prometheus's 185ms startup is not a problem for a long-running monitoring server. But it reveals something about operational weight. Prometheus initializes a TSDB, reads WAL segments, replays checkpoints, and sets up a scrape manager before it is ready. This is appropriate for a persistent storage system. Ding initializes a rule engine, parses a YAML file, and opens a TCP socket. The difference in startup time is a proxy for the difference in operational complexity.

The 20× gap has a practical consequence for crash recovery. If a monitoring process crashes, you want it back as fast as possible. A 9ms restart is indistinguishable from continuous operation. A 185ms restart (or more, on a loaded machine, for a tool that has more state to restore) means a window of unmonitored time.

Benchmark methodology note: Startup time was measured as wall-clock time from process fork to first successful GET /health (Ding) or GET /-/healthy (Prometheus). Ten runs each. Prometheus ran in Docker with the image pre-pulled, representing a warm-cache cold start.


Finding 5: Configuration Complexity — 12 Lines, One File

The numbers: A minimal working Ding configuration that monitors one metric and sends alerts to one webhook is 12 lines in 1 file. The equivalent Prometheus setup — a prometheus.yml, a rules.yaml, and an alertmanager.yml — is 30 lines across 3 files.

Why this matters — the surface area argument: The number of configuration lines is a proxy for the cognitive load required to understand and maintain a system. More lines means more things that can be misconfigured. More files means more places to look when something is wrong. Three separate configuration files also implies three separate conceptual models: the scrape configuration, the alerting rules, and the notification routing. Each has its own syntax and its own documentation.

Ding's single file covers all three concerns: what to watch, what condition to alert on, and where to send the alert. A developer new to the codebase can read the entire alerting configuration in the same amount of time it takes to read a short function.

Why this matters — the co-location argument: Prometheus's configuration files live outside the application codebase, typically managed by an ops team in a separate repository. Ding's ding.yaml lives in the application repository, committed alongside the code that emits the metrics. This is not just a convenience — it is a different model of ownership. When the developer who writes emit_metric("payment_timeout", 1) also writes the rule if payment_timeout > 5 alert, the alerting is a first-class artifact of the feature, not an afterthought filed with the infrastructure team.

Why the 2.5× ratio understates the real gap: The 30-line Prometheus count is for the minimum viable configuration. A production Prometheus setup typically adds receiver configurations, inhibition rules, route trees with matchers, and multiple alerting rules per prometheus.yml. Real-world configs run into the hundreds of lines. Ding's configuration grows with the number of rules, but each rule is self-contained — adding a rule does not require understanding an existing routing tree.


Finding 6: Latency Distribution — Tight, Predictable, With a Known Spike Pattern

The numbers: Across 100 latency samples, Ding's distribution was:

  • Minimum: 4ms
  • Median (p50): 4ms
  • p99: 16ms
  • Maximum: 16ms

The vast majority of samples were 4–5ms, with one outlier at 16ms on the first run (almost certainly a JIT warmup or OS scheduler artifact) and a handful of 5–6ms values throughout.

Why the tight distribution matters: A p99 of 16ms against a p50 of 4ms is a ratio of 4×. In latency-sensitive systems, a high p99/p50 ratio is often a sign of hidden contention, GC pauses, or resource saturation. A 4× ratio from 4ms to 16ms is very tight — it means the system behaves consistently even in the tail. There are no 100ms outliers, no 500ms spikes, nothing that suggests head-of-line blocking or queue saturation at these load levels.

Why the 16ms first-sample spike is expected: The first sample in every cold run tends to be the slowest because the OS has not yet established a cached routing path to the webhook receiver, the Go runtime has not yet compiled the hot path, and the kernel's scheduler has not yet determined the right CPU affinity for the goroutines. This is a well-understood artifact of microbenchmarking, not a sign of instability.

What this tells you about real-world behavior: A p99 of 16ms means that even in the worst case measured, an alert fires within a human reaction time. An on-call engineer who sets their phone down cannot perceive the difference between a 4ms alert and a 16ms alert. What they do perceive is the difference between a 4ms alert and a 62-second alert.


Methodology and Caveats

What was measured directly:

  • Ding alert latency: 100 end-to-end runs on localhost, timing from HTTP send to webhook receipt
  • Ding throughput: hey load test, 50 workers, 30 seconds
  • Ding startup: 10 process-fork-to-health-check runs
  • Ding config lines: counted from a real working config
  • Go engine benchmarks: go test -bench -benchtime=5s on an Apple M3

What was measured from reference sources:

  • Prometheus default latency: derived from documented default scrape interval (15s), eval interval (15s), and Alertmanager group_wait (30s), cross-checked with a 5-run manual measurement
  • Prometheus throughput: published Prometheus remote_write benchmark figures (protobuf format)
  • Prometheus config lines: counted from a minimal working prometheus.yml + rules.yaml + alertmanager.yml

Platform note: All measurements were taken on an Apple M3 (arm64, macOS). Absolute numbers will vary on different hardware. Relative figures — especially the latency ratio — are architecture-independent because they reflect structural differences (push vs. pull, stateless vs. stateful) not hardware differences.

What was not measured:

  • Memory footprint (Docker dependency not available in this run; deferred)
  • Ding stdin throughput (requires pv, not installed)
  • Prometheus minimum-latency configuration (requires Docker Compose stack; deferred)
  • Datadog agent latency and throughput (requires a Datadog account)
  • Behavior under sustained high cardinality (many unique label combinations)

Conclusion

The benchmark data tells a consistent story. Ding is not a general-purpose observability platform trying to compete with Prometheus on every dimension — it is a tool with a different architecture designed for a different use case, and the numbers reflect that design.

The 15,500× latency advantage is not because Ding is cleverly optimized. It is because Ding evaluates events the moment they arrive, while Prometheus is architecturally required to wait for a scrape cycle. The 20× startup advantage is not because Ding is carefully tuned. It is because Ding has no persistent state to initialize. The configuration simplicity is not an accident. It is the result of collapsing three separate configuration domains (scrape, rules, routing) into one.

Each of these advantages comes with a corresponding limitation. Ding has no historical storage, no cross-host aggregation, and no persistent audit trail. These are not defects — they are trade-offs, made deliberately in service of a specific goal: giving a single developer the ability to ship reliable alerting in minutes, on a machine they already have, without running anything else.

The benchmarks argue that Ding delivers on that goal. A 4ms alert latency, 116k rps throughput, a 9ms cold start, and a 12-line config are not just good numbers. They are evidence that the architectural choices — push over pull, stateless over stateful, single binary over distributed system — produce a tool that is genuinely faster and simpler to operate for the use cases it targets.


Raw results: benchmarks/results/latest.json Run script: benchmarks/comparison/run.sh Go benchmarks: benchmarks/go/bench_test.go