03 / 06PulseGo · gRPC · fsync
How Pulse sustains 282 messages/sec under 32-way concurrency in a single-node broker
A single-node event broker with a durable, checksummed log, gRPC API and CLI.
- publish throughput at concurrency 32
- 282 msg/s
- Source:
assets/bench-publish.json, 2026-09-02
The problem
I wanted a message broker I owned from the log format up, not a hosted queue I only configure. Most teams reach for a hosted broker before they know whether they need clustering at all, and pay for guarantees they never exercise. I set out to build the single-node case first: an event broker with a durable, checksummed log, a gRPC API, a CLI and a public Go client, for services that need ordered replay without running a cluster.
The mechanism
Pulse follows a plain layering. A caller drives it through pulse-cli or through pkg/client, the public Go client both the CLI and any external Go program depend on. Both talk gRPC to internal/adapters/grpc, which exposes the Broker and PubSub services and does nothing but translate wire types. The application layer, internal/application/services, holds the actual broker logic: topic management, publish and subscribe, and the compaction and retention sweepers. Underneath, each partition is one durable log in internal/infrastructure/storage/engine/log, backed by append-only segments and a sparse offset index, with every batch framed and checksummed by engine/codec using a CRC-32C from engine/checksum. A torn write on crash is caught by that checksum and truncated back to the last valid batch during recovery, rather than served as a partial record.
internal/ and pkg/client in the repo.I benchmarked Pulse against NATS JetStream with the same paired harness, bench/harness.go, one run on the same machine: same Docker Desktop, both binaries and data on the container filesystem, 3000 publishes per point, 256-byte payloads, sync-mode: every-write (fsync on every batch) on both sides. Hardware is not recorded beyond that.
| target | concurrency 1 | concurrency 8 | concurrency 32 |
|---|---|---|---|
| Pulse | 243 msg/s | 235 msg/s | 282 msg/s |
| NATS JetStream | 266 msg/s | 291 msg/s | 356 msg/s |
What it did
At concurrency 32, 3000 publishes of 256-byte messages against Pulse's own segment log completed at 282 messages per second with a p50 latency of 113.6 ms, both fsyncing every batch. That is the row I stand behind, from assets/bench-publish.json, dated 2026-09-02. The same file shows JetStream ahead of Pulse at every concurrency level on this box under this one configuration, which puts the two brokers in the same class here, not Pulse in front of it. It is a single run, not an average: a second run on the same machine landed at 349, 435 and 445 msg/s for Pulse while keeping roughly the same relative gap against JetStream, so the absolute numbers move between runs even though the comparison holds. I show both rows rather than only Pulse's, because a throughput number with no baseline is not proof of anything.
What I learned
Building the checksum and recovery path before the gRPC layer forced the durability guarantees to be real from the start instead of retrofitted: a torn write from a crash mid-fsync gets truncated deterministically because engine/codec rejects any batch whose CRC-32C does not match, not because a later patch added a check. The other lesson was about benchmarking itself. My first instinct was to publish a single Pulse number and call it done; running the same harness against JetStream, and running it twice, is what showed the number moves by 30 percent between runs on the same idle machine. A benchmark without a baseline and without a second run is not a number, it is a guess with three significant figures.
Sources
- README.md, 2026-09-04
- assets/bench-publish.json, date recorded in file: 2026-09-02
- docs/Client.md, 2026-09-04
- docs/Operations.md, 2026-09-04
- docs/Configuration.md, 2026-09-04
- CHANGELOG.md, 2026-09-04
- internal/infrastructure/storage/engine/codec/codec.go, 2026-09-04
- this site, content/projects/NUMBERS.md, 2026-09-04