The Serialization Bottleneck: Why JSON Kills High-Throughput Messaging

Building a message broker that can handle hundreds of thousands, let alone a million, messages per second presents a significant challenge, especially in Go. The primary culprit? Serialization and deserialization. When every message is parsed using standard formats like JSON, the application rapidly allocates a massive number of small objects in memory. This triggers frequent garbage collection cycles, which consume valuable CPU time and introduce unpredictable network latency spikes. For applications demanding extreme performance, this overhead is a non-starter.

The developer behind HermitMQ, Erkin Khidirov, identified this core problem. Instead of accepting the performance hit from conventional serialization methods, he opted for a radical simplification: a custom 29-byte binary protocol. This custom format drastically reduces the memory footprint and processing time associated with each message. Think of it less like a verbose, human-readable contract and more like a hyper-efficient, specialized handshake between two machines that only needs to convey essential data points.

Diagram illustrating the reduced overhead of a custom binary protocol versus JSON serialization.

Architectural Choices for Extreme Throughput

HermitMQ's architecture is built from the ground up for raw speed. Beyond the custom protocol, Khidirov implemented a direct file-to-socket copy mechanism for data transmission. This technique bypasses several layers of the standard network stack, reducing context switching and memory copies. Effectively, it allows the operating system to move data directly from a file descriptor (representing the message data) to a network socket descriptor with minimal intervention from the application code. This is akin to having a dedicated express lane for data, bypassing normal traffic control.

The broker's core logic is written entirely in Go, leveraging its concurrency primitives like goroutines and channels. However, the focus is on minimizing the work done within these goroutines. Instead of complex processing, they primarily facilitate the efficient movement of data. This design philosophy ensures that the broker can sustain a high message rate without becoming a CPU-bound bottleneck.

Data Storage and Persistence

For a message broker, persistence is crucial. HermitMQ employs a log-structured merge-tree (LSM tree) based approach for storing messages. This means data is primarily written to an append-only log file. When this log reaches a certain size, it is compacted and merged with older data into more optimized segments. This write-optimized strategy is ideal for high-ingestion workloads. Reads, while potentially more complex due to the merged segments, are optimized for the specific access patterns of a message broker, such as retrieving messages for a specific consumer group.

The persistence layer is designed to be as efficient as possible, integrating with the overall data transmission strategy. By using memory-mapped files and careful I/O management, the broker aims to minimize disk I/O latency, which is often a performance killer in traditional systems. The goal is to make disk operations as fast as network operations, a difficult but necessary feat for achieving the target throughput.

Benchmarking and Performance Metrics

The ultimate test for any high-performance system is its benchmark. HermitMQ was tested on a single AWS EC2 `c6gn.xlarge` instance, equipped with a 4-core Graviton2 processor. The benchmark setup involved a simple producer and consumer, both running on the same machine as the broker. This configuration eliminates network latency between the client and the broker, focusing purely on the broker's internal processing and I/O capabilities.

The results are striking: the broker consistently achieved a throughput of over 1 million messages per second. The messages themselves were small, approximately 100 bytes each, including headers and payload. This demonstrates the effectiveness of the custom protocol and efficient I/O. The benchmark also measured latency, which remained remarkably low, typically under 1 millisecond. This low and consistent latency is a direct consequence of minimizing garbage collection and optimizing data transfer paths.

Graph showing HermitMQ's throughput in messages per second under load.

Code Implementation Insights

The GitHub repository for HermitMQ provides a transparent look into its implementation. Developers can examine the Go code to understand how the custom binary protocol is defined and handled. The `netpoll` package, for instance, likely plays a key role in the efficient network I/O, potentially utilizing epoll or kqueue for high-performance event-driven networking. The data structures for managing message queues and consumer offsets are also critical, designed for concurrent access and minimal locking contention.

Examining the code reveals a pragmatic approach to performance engineering. There are no unnecessary abstractions. Every line of code appears to serve a purpose directly contributing to speed. This includes custom memory allocators or pooling strategies, although the primary focus remains on reducing allocations through the custom protocol and optimized I/O. The project serves as a case study for building high-performance distributed systems in Go, emphasizing the importance of understanding and controlling the entire I/O and serialization pipeline.

The Unanswered Question: Scalability Beyond a Single Node

While achieving 1 million messages per second on a single node is an impressive feat, the critical next step for any production-ready message broker is horizontal scalability. How does HermitMQ handle distribution across multiple nodes? What are the challenges in maintaining message ordering, fault tolerance, and consistency in a clustered environment? The current implementation, focused on single-node raw throughput, leaves these questions open. Building a distributed system on top of this high-performance core introduces a new set of complexities related to consensus, leader election, and inter-node communication that will undoubtedly test the limits of the custom protocol and I/O strategies.