Quicx Documentation
Installation
Quicx ships as a single static binary. One curl command detects your OS and architecture, verifies the checksum, and drops the daemon into your PATH.
The installer is a small POSIX sh script — it works on Linux (x86_64 / arm64) and macOS (Intel / Apple Silicon) without any additional tooling. No runtime dependencies, no package manager, no JDK. The Java client is distributed separately through Maven Central (see Quick Start).
curl -fsSL https://quicx.dev/install.sh | shPrefer to inspect the script first? Download it, read it, then run it — everything the installer does is visible plaintext:
curl -fsSL https://quicx.dev/install.sh -o install-quicx.sh
less install-quicx.sh
sh install-quicx.shWhat the installer does
- 1 · detect
- Reads
uname -sanduname -mto pick the right artifact —linux-x86_64,linux-arm64,darwin-x86_64ordarwin-arm64. - 2 · fetch
- Downloads the signed release tarball from
releases.quicx.devover HTTPS. - 3 · verify
- Checks the SHA‑256 against the signed
SHA256SUMSfile. Exits non-zero if the hash does not match. - 4 · install
- Places the
quicxbinary in/usr/local/bin(or~/.local/binif the user does not have root). Writes a defaultquicx.confto~/.config/quicx/. - 5 · verify
- Runs
quicx versionand prints the resolved install path so you know thePATHlookup works.
Verify it worked
user@host ~ % quicx version
quicx v1.0.0
quicx.conf from Configuration alongside it. No network access is required at runtime — Quicx never calls home.Quick Start
Start the daemon with quicx start, pull the Java client from Maven Central, and submit your first task. End-to-end in under a minute.
- Step01
Start the daemon
Quicx needs a configuration file to know which TCP port to bind and how to carve up the PMAD pool. The installer writes a sensible default to
~/.config/quicx/quicx.conf— see Configuration for every knob.~ $user@host ~ $ quicx start --config /etc/quicx/quicx.conf config loaded: /etc/quicx/quicx.conf quicx v1.0.0 starting port: 16381 classes: 32 64 128 256 512 1024 quicx listening on port 16381 [kqueue] quicx cli socket: /tmp/quicx.sock - Step02
Add the Java client to your build
The client lives on Maven Central under
dev.quicx:quicx-client. It’s a tiny jar (no transitive dependencies) that speaks the Quicx binary protocol directly.pom.xml<dependency> <groupId>dev.quicx</groupId> <artifactId>quicx-client</artifactId> <version>1.0.0</version> </dependency> - Step03
Submit your first task
QuicxClientis a stateless, thread-safe producer handle. Everysubmit()opens a connection, sendsMSG_SUBMIT, reads the acknowledgment, and closes — so you can treat it exactly like an HTTP call. The full wire format is documented in Binary Protocol.Producer.javaimport dev.quicx.QuicxClient; public class Producer { public static void main(String[] args) throws Exception { try (QuicxClient client = new QuicxClient("localhost", 16381)) { int taskId = client.submit( "send_email", "{\"to\":\"user@gmail.com\"}" ); System.out.println("accepted task id = " + taskId); } } } - Step04
Run a worker
QuicxWorkerconnects, announces itself withMSG_READYand then blocks receivingMSG_TASKframes. Register one handler per task type; the worker reconnects automatically on daemon restarts.EmailWorker.javaimport dev.quicx.QuicxWorker; public class EmailWorker { public static void main(String[] args) throws Exception { new QuicxWorker("localhost", 16381) .handle("send_email", payload -> { String body = new String(payload, "UTF-8"); System.out.println("delivering: " + body); // ... do the work ... }) .start(); // blocks — runs until the process is killed } }
MSG_SUBMIT with a 6‑byte header and a typed payload, then waited for a 4‑byte task_id. The daemon allocated every scratch buffer out of the PMAD pool — nomalloc, no GC pause — and handed the task to an idle worker. One daemon. No moving parts.Configuration
A Quicx daemon reads its entire runtime shape from a single quicx.conf file. Every number is exact — block counts, pool share, port — and is resolved before a byte of task traffic moves.
# Quicx config
[server]
port = 16381
[allocator]
pool_size = 1048576
class = 32,10
class = 64,25
class = 128,25
class = 256,20
class = 512,12
class = 1024,8[server]
- port
- TCP port the daemon binds to. Producers and workers both dial the same port — routing is decided by the first message they send (
MSG_SUBMITvsMSG_READY). Default:16381.
[allocator]
The allocator block is parsed directly into the PMAD initializer. You own every byte of the pool — no hidden reserves, no growth. All arithmetic is performed at startup so the daemon either boots with the layout you asked for or refuses to start.
- pool_size
- Total bytes reserved via a single
mmapcall. Default is1048576(1 MiB). The allocator never grows past this number — if you exhaust it, newMSG_SUBMITframes are rejected withMSG_ERROR 0x01 (queue full). - class = SIZE,PCT
- Declares one size class: SIZE is the block size in bytes, PCT is the percentage of the pool that belongs to that class. Declare the classes in ascending SIZE order. The percentages must sum to
100; if they don’t, the daemon refuses to start.
pool_size = 1 MiB with the six classes above resolves to exactly 3 276 · 32B + 4 096 · 64B + 2 048 · 128B + 819 · 256B + 245 · 512B + 82 · 1024B — all computed before the daemon accepts its first connection.Tuning rules of thumb
| Workload | Recommended shape | Why |
|---|---|---|
| Short JSON payloads (webhooks, emails) | 32, 64, 128 | Most MSG_SUBMIT frames land between 40–120 bytes; three tight classes eat the long tail with <5 % slack. |
| Mixed media (thumbnails, ML prompts) | 128, 512, 2048 | Two orders of magnitude spread — weight the biggest class heaviest. |
| Uniform binary blobs | one class at 100 % | Zero internal fragmentation. Highest throughput — see the PMAD benchmarks. |
pool_sizeis not a multiple of every declared class size, or if the percentages don’t sum to 100, the daemon exits with a precise error pointing at the offending line. This is deliberate — Quicx refuses to start in a “mostly-correct” state.Architecture
One daemon, three role-based endpoints, one allocator. Every moving piece is visible in a single diagram — and intentionally, no piece is optional.
Quicx is deliberately flat. A single daemonprocess owns the task queue, the worker pool and the PMAD allocator. Producers and workers are plain TCP clients that speak the same binary protocol — the first frame they send tells the daemon which role they’re playing.
There are exactly three horizontal message paths:
- producer → daemon
MSG_SUBMITlands a new task. The daemon responds with eitherMSG_OK {task_id}orMSG_ERROR {code}— always, within one round-trip. No streaming, no batching, no surprises.- daemon → worker
- Workers announce themselves with
MSG_READYand block reading. The daemon pushesMSG_TASKframes to the first idle worker. If the queue is empty, the daemon replies withMSG_WAITinstead of keeping a pending read open. - worker → daemon
MSG_DONE {task_id}on success,MSG_FAILED {task_id, reason}on failure.MSG_HEARTBEAT/MSG_PONGkeep the socket from half-closing under long idle.
Why a single daemon?
Multi-node queues pay a tax in the form of leader elections, replication logs and consistent hashing. Quicx is optimised for the much more common case where your queue lives on the same host (or at worst, the same availability zone) as your producers and workers. One daemon is enough to saturate a 10 GbE NIC with short tasks and — because of PMAD — it does so with zero allocation jitter under sustained load.
Scaling horizontally means running multiple independent Quicx daemons behind a simple TCP load balancer. Because every socket is stateless at the protocol level (a submit is one request, one reply), there is no session to pin and no replication to coordinate.
PMAD — Predictive Memory Allocator
A slab allocator written in C that delivers O(1) allocation and deallocation with zero fragmentation and zero system calls at runtime. Every allocation the daemon makes — task envelopes, wire buffers, worker registration slots — comes out of PMAD. Fragmentation is 0 % by design: every block is pre-sized to a declared class, so there is no splitting, no coalescing, and no wasted space.
PMAD pre-allocates a contiguous pool of memory with a single mmap call at startup, then partitions it into user-defined size classes. Standard allocators (ptmalloc, jemalloc v5.3, tcmalloc v2026) optimise for average-case throughput — PMAD optimises for worst-case determinism and predictable latency budgets.
| Domain | Why PMAD fits |
|---|---|
| Real-time systems | Guaranteed O(1) response — no lock contention, no syscalls at runtime |
| Embedded / RTOS | Minimal footprint, no heap fragmentation, fully configurable memory layout |
| Game engines | Predictable frame-time budgets with zero allocation jitter |
| High-frequency trading | Nanosecond-class allocation latency under sustained throughput |
Architecture
Every allocation is a single lookup-table index followed by a free-list pop. Every deallocation is a free-list push keyed by the block’s own header. Both operations have no conditional branch paths — the fast path is the only path.
- Public API
- The thin facade in
incPMAD.h—pmad_init,pmad_alloc,pmad_free,pmad_destroy. This is the entire contract the daemon consumes. - Size Class Table
- A flat array
[MAX_SIZE / ALIGNMENT]maps a requested byte-count directly to the correct size-class descriptor — an O(1) table lookup, no branches. - Free Lists
- Each size class owns a singly-linked intrusive free list. A pop is a pointer dereference; a push is a pointer swap. No atomics on the fast path — the daemon serialises through its own router, so locks are structurally unnecessary.
- Memory Pool
- One
mmapregion split into contiguous runs of blocks, one run per class, sized by the user percentages. Each block carries a 16-byteBlockHeader(nextpointer + class ID) so deallocations need no external metadata.
Benchmarks
Measured on Apple Silicon (-O3 -march=native). Full benchmark source and reproduction instructions are available on github.com/anastassow/PMAD.
| Metric | Value |
|---|---|
| Sustained allocation latency | 19.1 ns |
| Peak throughput | >460 M ops/s |
| Jitter (σ) | 0.0 ns (deterministic) |
| Fragmentation | 0 % |
| Runtime syscalls | Zero |
| Configurability | Fully user-defined size classes |
Reference configurations
| Profile | Size classes (B) | Split (%) | Avg. latency | Throughput | Suitability |
|---|---|---|---|---|---|
| Max throughput | {16} | 100 | 19.1 ns | 436.9 M/s | Small-object velocity |
| Min overhead | {4096} | 100 | 19.7 ns | 254.0 M/s | Bulk data density |
| Balanced | {64, 256, 1024} | 60 / 30 / 10 | 20.6 ns | 462.6 M/s | Mixed workloads |
| Latency-optimised | {32, 128} | 80 / 20 | 19.8 ns | 426.2 M/s | Critical signalling |
| HFT / network | {32, 128, 512, …} | 60 / 20 / … | 24.7 ns | 397.2 M/s | L3 packet processing |
| Embedded / RTOS | {8, 16, 32, …} | 30 / 30 / … | 22.3 ns | 327.7 M/s | Deterministic control |
pmad_alloc is as fast as your first.Tear-down
A single munmap returns the entire pool to the OS in O(1) — there are no individual blocks to walk, no fragmented regions to compact. Shutdown is symmetric with startup: one syscall in, one syscall out.
Binary Protocol
Every frame on the wire is a 6-byte header followed by a variable-length payload. No framing ambiguity, no partial reads, no text encoding — parsing is a couple of pointer reads.
Frame header
- version
- Protocol revision. Currently 0x01. The daemon rejects any other version with MSG_ERROR 0x02 so protocol evolution is additive and opt-in.
- type
- Message opcode — one of the 11 types below. The daemon routes on type alone; producers and workers speak the same header shape.
- length
- 32-bit big-endian unsigned integer: the payload size in bytes. Zero is legal for MSG_READY, MSG_WAIT, MSG_HEARTBEAT, MSG_PONG and MSG_STATS.
- payload
- Exactly length bytes. The per-type layouts below are the full contract — there is no escaping, no delimiters and no padding.
Message types
| Type | Name | Direction |
|---|---|---|
0x01 | MSG_SUBMIT | producer → daemon |
0x02 | MSG_OK | daemon → producer |
0x03 | MSG_ERROR | daemon → producer |
0x04 | MSG_READY | worker → daemon |
0x05 | MSG_TASK | daemon → worker |
0x06 | MSG_DONE | worker → daemon |
0x07 | MSG_FAILED | worker → daemon |
0x08 | MSG_WAIT | daemon → worker |
0x09 | MSG_HEARTBEAT | either direction |
0x0A | MSG_PONG | either direction |
0x0B | MSG_STATS | monitor → daemon |
0x0C | MSG_STATS_RESPONSE | daemon → monitor |
Payload formats
[type_len : 1 byte][type : type_len bytes][payload : rest of bytes]type = "send_email"
payload = {"to":"user@gmail.com"}
bytes = [10][send_email][{"to":"user@gmail.com"}][task_id : 4 bytes]task_id = 0x00000A42 → accepted task id = 2626[error_code : 1 byte][message : rest of bytes]| Code | Meaning |
|---|---|
0x01 | queue full — PMAD pool exhausted |
0x02 | invalid message (bad version / length / type) |
0x03 | payload too large for the largest size class |
0x04 | unknown task type |
(no payload — length = 0)Sent once per connection, immediately after connect, to register
the socket as an idle worker. The daemon replies with MSG_TASK or
MSG_WAIT.[task_id : 4 bytes][type_len : 1 byte][type : type_len bytes][payload : rest]Mirror of MSG_SUBMIT with the task id prepended. The worker dispatches
by type and replies with MSG_DONE or MSG_FAILED carrying the same id.[task_id : 4 bytes]Confirms successful completion. The daemon frees the task slot back
to PMAD before responding to any producer waiting on this id.[task_id : 4 bytes][reason : rest of bytes]reason is a UTF-8 string propagated verbatim to producers that
observe the task, and logged by the daemon. Keep it short — it lives
in the same size class as the original payload.(no payload — length = 0)Sent in place of MSG_TASK when the queue is empty. The worker keeps
the socket open and issues another MSG_READY after a short backoff.(no payload — length = 0)Liveness probe. Either side may send it; the receiver replies with
MSG_PONG. Intended for long-idle worker sockets behind stateful
load balancers.(no payload — length = 0)The only valid reply to MSG_HEARTBEAT. Shape-symmetric with
MSG_HEARTBEAT for trivial framing.(no payload — length = 0)Requests a one-shot metrics snapshot. The daemon responds with
MSG_STATS_RESPONSE.[queue_depth : 4 bytes][workers_total : 4 bytes][workers_idle : 4 bytes][pmad_bytes_used : 8 bytes][pmad_bytes_total : 8 bytes]All integers are big-endian. The 28-byte body is a fixed shape so
dashboards can parse it without a schema.CLI Reference
quicx is the single binary that ships with the release. It is self-documenting — running it with no arguments prints the same usage you see below.
quicx v1.0.0 — lightweight task queue daemon
usage:
quicx start --config FILE
quicx stop
quicx status
quicx versionquicx start --config FILEBinds the port declared in [server], maps the PMAD pool, and begins accepting connections. Runs in the foreground — the calling shell owns the process. Pair with systemd, tmux, launchd or your container supervisor for lifecycle management. See Configuration for the file format.
quicx stopSends SIGTERM to the pid recorded in/var/run/quicx.pid (or $XDG_RUNTIME_DIR/quicx.pid for non-root installs). The daemon drains in-flight tasks, munmaps the PMAD pool and exits cleanly.
quicx statusOpens a short-lived control connection over the /tmp/quicx.sock Unix socket and sends MSG_STATS, then renders the MSG_STATS_RESPONSE as a human-readable table. Shows uptime, worker pool state (idle / busy / total), queue depth, task counters (submitted / completed / failed), memory usage against the configured pool, and a per-size-class PMAD slab breakdown with utilisation bars. Safe to script — exits non-zero if the daemon is unreachable.
user@host ~ $ quicx status
quicx v1.0.0
─────────────────────────────────────────
uptime 0h 0m 6s
workers idle: 0 busy: 0 total: 0
queue waiting: 0
tasks submitted: 0
completed: 0
failed: 0
memory 32 / 913408 bytes (0.0%)
PMAD:
32B [░░░░░░░░░░░░░░░░░░░░] 1 / 2184
64B [░░░░░░░░░░░░░░░░░░░░] 0 / 3276
128B [░░░░░░░░░░░░░░░░░░░░] 0 / 1820
256B [░░░░░░░░░░░░░░░░░░░░] 0 / 770
512B [░░░░░░░░░░░░░░░░░░░░] 0 / 238
1024B [░░░░░░░░░░░░░░░░░░░░] 0 / 80
user@host ~ $quicx versionPrints the semver, build date and target triple. Machine-parsable if you pipe it — one line, space-separated.
Java Client
dev.quicx:quicx-client is a small, dependency-free Java 11+ library. Two classes carry the whole surface area: QuicxClient for producers, QuicxWorker for consumers.
Add it to your build
<dependency>
<groupId>dev.quicx</groupId>
<artifactId>quicx-client</artifactId>
<version>1.0.0</version>
</dependency>QuicxClient — producers
QuicxClient is stateless at the connection level: every call to submit()opens a fresh TCP connection, performs the submit request-reply, and closes. Keep the object around for the lifetime of the producer — it’s safe to reuse and share across threads.
- new QuicxClient(host, port)
- Construct a reusable handle. No network work is performed here.
- int submit(type, byte[])
- Send a
MSG_SUBMITwith a raw payload. Returns the 32-bit task id assigned by the daemon. ThrowsQuicxExceptiononMSG_ERROR. - int submit(type, String)
- Convenience overload: UTF-8 encodes the payload for you.
- close()
- Idempotent. Tears down any transport resources. Use try-with-resources.
import dev.quicx.QuicxClient;
import dev.quicx.QuicxException;
try (QuicxClient client = new QuicxClient("localhost", 16381)) {
int id = client.submit(
"resize_image",
new byte[]{ 0x01, 0x02, 0x03 /* raw bytes */ }
);
System.out.println("accepted id = " + id);
} catch (QuicxException e) {
// daemon rejected the task — rate-limit, retry or surface upstream
System.err.println("rejected: " + e.getMessage());
}QuicxWorker — consumers
QuicxWorker is a long-lived connection that receives MSG_TASK frames. Register a handler per task type; the worker dispatches by string key and replies to the daemon with MSG_DONE or MSG_FAILED automatically.
- new QuicxWorker(host, port)
- Construct a worker. No network work happens until start().
- handle(type, handler)
- Register a
TaskHandlerfor a task type. Returnsthisfor chaining. Handlers are keyed by the sametypestring the producer sent inMSG_SUBMIT. - start()
- Connect, send
MSG_READYand enter the dispatch loop. Blocks forever. On an unexpected disconnect the worker sleeps for 3 s and reconnects — your handlers keep running across daemon restarts. - close()
- Flips the running flag, closes the socket and unblocks start(). Safe to call from a shutdown hook.
import dev.quicx.QuicxWorker;
public class Worker {
public static void main(String[] args) throws Exception {
QuicxWorker worker = new QuicxWorker("localhost", 16381)
.handle("send_email", payload -> {
String body = new String(payload, "UTF-8");
EmailService.deliver(body);
})
.handle("resize_image", payload -> {
Images.resize(payload);
});
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try { worker.close(); } catch (Exception ignored) {}
}));
worker.start(); // blocks — the dispatch loop owns this thread
}
}MSG_FAILED with exception.getMessage() as the reason — the task is not retried automatically. If no handler is registered for an incoming type, the worker logs and sends MSG_FAILED with “no handler for: …”.QuicxException
An unchecked RuntimeException thrown by QuicxClient#submiton rejection or protocol error. It wraps the daemon’s MSG_ERROR message string so the cause is visible without decoding bytes by hand.
package dev.quicx;
public class QuicxException extends RuntimeException {
public QuicxException(String message) { super(message); }
public QuicxException(String message, Throwable cause) {
super(message, cause);
}
}Changelog
Quicx follows semver. Breaking protocol changes bump the major version; new message types are additive and bump minor. Patch releases are build-or-docs-only.
- v1.0.02026-04-21
- First public release.
- Binary protocol frozen — 12 message types, 6-byte header, versioned.
- PMAD v1 — O(1) slab allocator with user-defined size classes.
- Java client published to Maven Central as dev.quicx:quicx-client.
- Install script for Linux (x86_64, arm64) and macOS (Intel, Apple Silicon).
SHA256SUMS.Configure it once. Run it forever.
One binary, one TOML file, a Java jar on Maven Central. The rest is just taking tasks off a queue.