Quicx

Quicx Documentation

§ 01.01Getting Started

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).

install with curl
curl -fsSL https://quicx.dev/install.sh | sh

Prefer to inspect the script first? Download it, read it, then run it — everything the installer does is visible plaintext:

inspect first, install later
curl -fsSL https://quicx.dev/install.sh -o install-quicx.sh
less install-quicx.sh
sh install-quicx.sh

What the installer does

1 · detect
Reads uname -s and uname -m to pick the right artifact — linux-x86_64, linux-arm64, darwin-x86_64 or darwin-arm64.
2 · fetch
Downloads the signed release tarball from releases.quicx.dev over HTTPS.
3 · verify
Checks the SHA‑256 against the signed SHA256SUMS file. Exits non-zero if the hash does not match.
4 · install
Places the quicx binary in /usr/local/bin (or ~/.local/bin if the user does not have root). Writes a default quicx.conf to ~/.config/quicx/.
5 · verify
Runs quicx version and prints the resolved install path so you know the PATH lookup works.

Verify it worked

~ $
user@host ~ % quicx version
quicx v1.0.0
TIPOffline / airgapped installs
Download the release archive directly from github.com/anastassow/Quicx/tree/main/releases, copy the binary into your image, and drop the quicx.conf from Configuration alongside it. No network access is required at runtime — Quicx never calls home.
§ 01.02Getting Started

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.

  1. 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
  2. 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>
  3. Step03

    Submit your first task

    QuicxClient is a stateless, thread-safe producer handle. Every submit() opens a connection, sends MSG_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.java
    import 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);
            }
        }
    }
  4. Step04

    Run a worker

    QuicxWorker connects, announces itself with MSG_READY and then blocks receivingMSG_TASK frames. Register one handler per task type; the worker reconnects automatically on daemon restarts.

    EmailWorker.java
    import 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
        }
    }
PERFORMANCEWhat just happened?
The producer opened a short-lived TCP connection, framed a 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.
§ 01.03Getting Started

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.conf
# 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_SUBMIT vs MSG_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 mmap call. Default is 1048576 (1 MiB). The allocator never grows past this number — if you exhaust it, newMSG_SUBMIT frames are rejected with MSG_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.
NOTEHow the example pool is carved up
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

WorkloadRecommended shapeWhy
Short JSON payloads (webhooks, emails)32, 64, 128Most 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, 2048Two orders of magnitude spread — weight the biggest class heaviest.
Uniform binary blobsone class at 100 %Zero internal fragmentation. Highest throughput — see the PMAD benchmarks.
HEADS UPValidation is strict on purpose
If 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.
§ 02.01Core Concepts

Architecture

One daemon, three role-based endpoints, one allocator. Every moving piece is visible in a single diagram — and intentionally, no piece is optional.

PMAD SLAB ALLOCATORO(1) alloc / freePRODUCERSStats MonitorProducer BProducer AQUICX DAEMONConnection RouterPROCESSINGTask QueueDispatcherWorker PoolWORKERSWorker 1Worker 2Worker NMSG_STATSMSG_SUBMITMSG_OKMSG_TASKMSG_DONEMSG_SUBMIT (producer → daemon)MSG_OK (acknowledgment)MSG_TASK (task dispatch)MSG_DONE / memory allocMSG_STATS / MSG_WAIT
QuicxOne daemon · zero moving parts

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_SUBMIT lands a new task. The daemon responds with either MSG_OK {task_id} or MSG_ERROR {code} — always, within one round-trip. No streaming, no batching, no surprises.
daemon → worker
Workers announce themselves with MSG_READY and block reading. The daemon pushes MSG_TASK frames to the first idle worker. If the queue is empty, the daemon replies with MSG_WAIT instead of keeping a pending read open.
worker → daemon
MSG_DONE {task_id} on success, MSG_FAILED {task_id, reason} on failure. MSG_HEARTBEAT / MSG_PONG keep 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.

§ 02.02Core Concepts

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.

DomainWhy PMAD fits
Real-time systemsGuaranteed O(1) response — no lock contention, no syscalls at runtime
Embedded / RTOSMinimal footprint, no heap fragmentation, fully configurable memory layout
Game enginesPredictable frame-time budgets with zero allocation jitter
High-frequency tradingNanosecond-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.

PMAD — Predictive Memory Allocator architecture overview
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 mmap region split into contiguous runs of blocks, one run per class, sized by the user percentages. Each block carries a 16-byte BlockHeader (next pointer + 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.

MetricValue
Sustained allocation latency19.1 ns
Peak throughput>460 M ops/s
Jitter (σ)0.0 ns (deterministic)
Fragmentation0 %
Runtime syscallsZero
ConfigurabilityFully user-defined size classes

Reference configurations

ProfileSize classes (B)Split (%)Avg. latencyThroughputSuitability
Max throughput{16}10019.1 ns436.9 M/sSmall-object velocity
Min overhead{4096}10019.7 ns254.0 M/sBulk data density
Balanced{64, 256, 1024}60 / 30 / 1020.6 ns462.6 M/sMixed workloads
Latency-optimised{32, 128}80 / 2019.8 ns426.2 M/sCritical signalling
HFT / network{32, 128, 512, …}60 / 20 / …24.7 ns397.2 M/sL3 packet processing
Embedded / RTOS{8, 16, 32, …}30 / 30 / …22.3 ns327.7 M/sDeterministic control
PERFORMANCEWhat these numbers actually mean
Zero jitteris not marketing — PMAD’s instruction path is identical for every allocation, so the only variance you can measure is system-level noise. Zero runtime syscalls means kernel scheduling never interrupts an allocation. Your 1 000 000th 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.

§ 02.03Core Concepts

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

total header = 6 bytes fixedtotal message = 6 + length bytes
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

TypeNameDirection
0x01MSG_SUBMITproducer → daemon
0x02MSG_OKdaemon → producer
0x03MSG_ERRORdaemon → producer
0x04MSG_READYworker → daemon
0x05MSG_TASKdaemon → worker
0x06MSG_DONEworker → daemon
0x07MSG_FAILEDworker → daemon
0x08MSG_WAITdaemon → worker
0x09MSG_HEARTBEATeither direction
0x0AMSG_PONGeither direction
0x0BMSG_STATSmonitor → daemon
0x0CMSG_STATS_RESPONSEdaemon → monitor

Payload formats

0x01MSG_SUBMITproducer → daemon
[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"}]
0x02MSG_OKdaemon → producer
[task_id : 4 bytes]
task_id = 0x00000A42  →  accepted task id = 2626
0x03MSG_ERRORdaemon → producer
[error_code : 1 byte][message : rest of bytes]
CodeMeaning
0x01queue full — PMAD pool exhausted
0x02invalid message (bad version / length / type)
0x03payload too large for the largest size class
0x04unknown task type
0x04MSG_READYworker → daemon
(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.
0x05MSG_TASKdaemon → worker
[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.
0x06MSG_DONEworker → daemon
[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.
0x07MSG_FAILEDworker → daemon
[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.
0x08MSG_WAITdaemon → worker
(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.
0x09MSG_HEARTBEATboth directions
(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.
0x0AMSG_PONGboth directions
(no payload — length = 0)
The only valid reply to MSG_HEARTBEAT. Shape-symmetric with
MSG_HEARTBEAT for trivial framing.
0x0BMSG_STATSmonitor → daemon
(no payload — length = 0)
Requests a one-shot metrics snapshot. The daemon responds with
MSG_STATS_RESPONSE.
0x0CMSG_STATS_RESPONSEdaemon → monitor
[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.
§ 03.01Reference

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
quicx v1.0.0 — lightweight task queue daemon

usage:
  quicx start --config FILE
  quicx stop
  quicx status
  quicx version
quicx start --config FILE
start the daemon in the foreground

Binds 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 stop
gracefully stop the local daemon

Sends 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 status
live observation of the running daemon

Opens 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 version
print the binary version + build metadata

Prints the semver, build date and target triple. Machine-parsable if you pipe it — one line, space-separated.

§ 03.02Reference

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

pom.xml
<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_SUBMIT with a raw payload. Returns the 32-bit task id assigned by the daemon. Throws QuicxException on MSG_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.
Producer.java
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 TaskHandler for a task type. Returns this for chaining. Handlers are keyed by the same type string the producer sent in MSG_SUBMIT.
start()
Connect, send MSG_READY and 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.
Worker.java
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
    }
}
NOTEError semantics
If a handler throws, the worker sends 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.

QuicxException.java
package dev.quicx;

public class QuicxException extends RuntimeException {
    public QuicxException(String message) { super(message); }
    public QuicxException(String message, Throwable cause) {
        super(message, cause);
    }
}
§ 03.03Reference

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-21Current
    • 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).
TIPSubscribe to release notes
The canonical source of release notes is the GitHub releases page. Watch the repository for notifications — every release bundles a signed tarball plus the matching SHA256SUMS.
Ready to run it?

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.