A high-performance, lock-free system metrics broker written in C, with a real-time React dashboard. IzelMQ implements a publish-subscribe messaging pipeline from scratch — no external libraries, no runtime dependencies — and exposes live CPU, memory, and network telemetry over a hand-rolled HTTP/1.1 server.
IzelMQ continuously samples Linux kernel metrics from /proc, publishes them through an internal message broker, and serves them as JSON over HTTP with sub-millisecond latency. A React frontend polls the endpoints every second and renders live sparklines, per-core CPU bars, and a 60-second rolling history chart.
/proc/stat → producer_cpu ─┐
/proc/meminfo → producer_memory ─┤→ broker → consumer → snapshot → HTTP API
/proc/net/dev → producer_network ─┘
Typical observed latencies on localhost:
avg_lat=14µs | p99=72µs | peak_lat=72µs | dropped=0
p99 (99th percentile latency) means 99% of all requests complete in under 72µs. The remaining 1% — the worst-case tail — never exceeds 72µs either. In production observability systems, p99 is the metric that matters: averages hide outliers, and outliers are what users feel.
Most developers reach for top, htop, or a Prometheus/Grafana stack when they want system metrics. IzelMQ is none of those things. It exists to answer a different question: what does it actually take to build a low-latency telemetry pipeline from first principles?
You can cat /proc/stat in a shell script and pipe it somewhere. That works. But it gives you raw counters with no rates, no history, no concurrency model, and no way to serve the data to anything else without rebuilding everything from scratch. IzelMQ turns those counters into a structured, queryable stream — computing deltas for rates, maintaining a rolling history, and making the data instantly available over HTTP without polling the filesystem on every request.
The standard production answer is Prometheus pulling from node_exporter every 15–60 seconds, stored in a time-series database, queried with PromQL, visualized in Grafana. That stack is excellent for infrastructure at scale. It is also: two separate binaries, a database process, a query language, a dashboarding service, and a scrape interval that makes 1-second resolution impractical.
IzelMQ serves fresh data in under 100µs with a single binary and zero configuration. The entire observability stack — collection, transport, storage (60-second ring buffer), and visualization — compiles to one executable and one npm run dev.
Go and Python are reasonable choices for this kind of tool. They would also make it trivially easy. The point of IzelMQ is the constraint: no garbage collector, no runtime, no framework. Every allocation is explicit. Every synchronization primitive is chosen deliberately. The lock-free paths are lock-free because the author understood the memory model well enough to make them so, not because a library handled it.
This matters because the techniques here — CAS loops, atomic state machines, slab allocators, edge-triggered I/O — are exactly what you find inside the systems that Go and Python are built on.
Bringing in RabbitMQ or Kafka to shuttle metrics between three threads on a single machine is like renting a warehouse to store a shoebox. Those systems solve distributed messaging across networks and processes. IzelMQ solves in-process pub/sub with a latency budget measured in microseconds. The broker here is a few hundred lines of C with no network overhead, no serialization format, no broker process — just atomic operations on shared memory.
| Shell script | node_exporter | IzelMQ | |
|---|---|---|---|
| Latency to serve data | N/A | ~1ms (HTTP) | 14µs avg, 72µs p99 |
| Dependencies | bash | Go runtime | none |
| Scrape resolution | manual | 15s minimum | 1s, configurable |
| Memory footprint | N/A | ~20MB | < 2MB |
| Setup | none | install + configure Prometheus | make |
| Allocator | libc malloc | Go GC | lock-free slab |
| I/O model | blocking | goroutines | epoll edge-triggered |
| Synchronization | none | Go channels | CAS / atomic state machine |
IzelMQ is not a replacement for Prometheus in production. It is proof that you can build the core of what Prometheus does — collect, buffer, serve — in under 2000 lines of C, with no dependencies, and have it respond faster than most production systems can even acknowledge a connection.
The broker is a topic-based pub/sub system built entirely on lock-free primitives. Producers publish typed messages to named topics; a single consumer thread drains the queues and commits snapshots for the HTTP layer to read.
Key design decisions:
- Lock-free message passing using
stdatomic.h— no mutexes in the hot path - Custom slab allocator (
ialloc) with separate small and big block pools, avoidingmallocoverhead in the metrics pipeline - Triple-buffer snapshot with CAS-based slot selection, allowing the HTTP server to read the latest snapshot without blocking producers
A minimal, epoll-based HTTP/1.1 server purpose-built for low-latency metric serving. Each request is handled inline in the epoll loop — no thread pool, no dynamic dispatch beyond a path comparison.
- Non-blocking sockets with
EPOLLET(edge-triggered) TCP_NODELAYon every client connectionSO_REUSEPORTon the listen socket- CORS headers for local dashboard access
- Structured request parsing with a hand-written protocol layer
Three POSIX threads, each sampling a different /proc file at 1-second intervals:
| Producer | Source | Fields |
|---|---|---|
producer_cpu |
/proc/stat |
overall %, per-core %, load averages |
producer_memory |
/proc/meminfo |
used, available, cached, buffers, swap |
producer_network |
/proc/net/dev |
per-interface rx/tx bytes, rates, errors, drops |
Rate metrics (e.g. rx_rate_bps) are computed as deltas between consecutive reads, giving true bytes-per-second values rather than cumulative counters.
A React 18 + Chart.js frontend with no state management library. All data flows through a single useMetrics hook that polls the three API endpoints concurrently with Promise.all. Key UI features:
- Animated number transitions (800ms
requestAnimationFrameinterpolation) - Per-core bar chart with height proportional to utilization
- 60-second rolling history with interactive crosshair tooltip
- Graceful disconnection states — shows last known data when the broker is unreachable
The server exposes four endpoints on localhost:4444:
| Endpoint | Description |
|---|---|
GET /metrics/cpu |
CPU utilization, load averages, per-core breakdown |
GET /metrics/memory |
Memory breakdown in KB with swap stats |
GET /metrics/network |
Per-interface rx/tx rates and cumulative counters |
GET /health |
Readiness check — returns warming_up until all producers have data |
All responses include Access-Control-Allow-Origin: * and Cache-Control: no-cache.
Example response from /metrics/cpu:
{
"cpu_pct": 4.1,
"cores": 20,
"load_1": 1.00,
"load_5": 1.23,
"load_15": 0.90,
"per_core": [6.0, 0.0, 8.0, 2.0, 8.1, 0.0, 6.1, 0.0, 8.0, 0.0, 11.1, 0.0, 6.1, 0.0, 4.1, 0.0, 6.9, 5.0, 3.1, 2.0]
}- Linux (reads from
/proc— macOS not supported) gcc,make,pthreads- Node.js ≥ 18, npm
# Clone the repo
git clone git@github.com:iliassovic2003/Izel_MQ.git
cd Izel_MQ
# Install frontend dependencies, then launch broker + Vite in a tmux split
makemake (with no target) runs install then dev, which:
- Installs npm packages in
dashboard/ - Opens a
tmuxsession with the C broker on the left andvite devon the right
Open http://localhost:5173 in your browser.
make build # compiles server + bundles dashboard to dashboard/dist/
make run # starts the brokerServe dashboard/dist/ with any static file server (nginx, caddy, etc.).
| Target | Description |
|---|---|
make |
Install dependencies + launch dev environment |
make build |
Compile server + bundle dashboard |
make build-server |
Compile C broker only |
make build-dashboard |
Bundle React app only |
make dev |
Launch broker + Vite in a tmux split |
make run |
Run the compiled broker |
make kill |
Kill the tmux session |
make clean |
Remove object files |
make fclean |
Full clean (binaries + node_modules + dist) |
make re |
fclean then build |
make asan |
Build with AddressSanitizer |
make tsan |
Build with ThreadSanitizer |
./srcs/izmq --DEBUGPrints per-request latency and periodic stats:
[REQ ] /metrics/cpu lat= 24µs
[REQ ] /metrics/memory lat= 8µs
[REQ ] /metrics/network lat= 7µs
[STAT] reqs=93 | avg_lat=13µs | p99=72µs | peak_lat=72µs | dropped=0
Izel_MQ/
├── dashboard
│ ├── eslint.config.js
│ ├── index.html
│ ├── package.json
│ ├── package-lock.json
│ ├── public
│ │ ├── favicon.svg
│ │ └── icons.svg
│ ├── README.md
│ ├── src
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── assets
│ │ │ ├── hero.png
│ │ │ ├── react.svg
│ │ │ └── vite.svg
│ │ ├── index.css
│ │ └── main.jsx
│ └── vite.config.js
├── docs
│ ├── Changelogs.md
│ └── dashboard.png
├── Makefile
├── README.md
└── srcs
├── core
│ ├── broker
│ │ ├── broker.c
│ │ └── broker.h
│ ├── ialloc
│ │ ├── big_pool.c
│ │ ├── lock_free_malloc.c
│ │ ├── lock_free_malloc.h
│ │ └── small_pool.c
│ ├── message
│ │ ├── message.c
│ │ └── message.h
│ └── queue
│ ├── queue.c
│ └── queue.h
├── http
│ ├── api.c
│ ├── api.h
│ ├── consumer.c
│ └── consumer.h
├── Makefile
├── metrics
│ ├── cpu.c
│ ├── memory.c
│ ├── metrics.h
│ └── network.c
├── server.c
└── transport
├── protocol.c
├── protocol.h
├── server.c
├── server.h
└── snapshot.h
15 directories, 44 files
Triple-buffer snapshot (snapshot.h, consumer.c, api.c)
The snapshot uses a 3-slot buffer with a packed atomic state word encoding read_slot, write_slot, dirty_slot, and a dirty_flag bit. The consumer picks the slot that is neither read nor dirty, writes into it, then atomically swaps it to dirty with a CAS loop. The HTTP handler atomically claims the dirty slot as the new read slot. This gives wait-free reads and contention-free writes with no locks.
Lock-free allocator (srcs/core/ialloc/)
Two fixed-size pools — small blocks for control messages, big blocks for data payloads — managed with atomic bitmasks. iloc(size) routes to the appropriate pool and returns a block in O(1) without calling malloc. ifree marks the block available with a single atomic store.
epoll edge-triggered server (server.c)
The server uses EPOLLET (edge-triggered mode), which means it must drain the accept queue in a loop on each EPOLLIN event. Each client connection is non-blocking; reads that return EAGAIN are treated as complete rather than errors. Connections are closed after a single request — HTTP/1.0 semantics — keeping the event loop simple.
MIT License
Copyright (c) 2025 Katchuru0_0
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
