Quickstart
TL;DR — Build, point
wab_dirat a directory you own, run. The daemon listens on a Unix socket and serves Prometheus metrics on127.0.0.1:9185. With no sink configured, records are accepted, acked, and drained to the built-in no-op sink (nothing downstream); configure a sink (HTTP / MySQL / Postgres / ClickHouse) to forward them.
This guide gets you to a running weir daemon in under five minutes, then walks through pushing your first record and reading the metrics. For installation options other than building from source, see install.md. For the full configuration surface, see configuration.md.
Prerequisites
- Rust 1.88+ (edition 2024) —
rustup toolchain install stable(1.88 is the declared MSRV, enforced in CI) - Unix host to run the DAEMON — Linux or macOS.
weir-serveris not built for Windows: the listener layer is#[cfg(unix)], so it would accept nothing.weir-coreis genuinely cross-platform, and since 2.0.3 so is the client's TCP + mutual-TLS transport: a Windows producer can useweir-client --features tlsagainst a Linux/macOS daemon. OnlyWeirClient::connect, the Unix-socket constructor, is Unix-only. - A writable directory — for the WAB segments. Anywhere works;
the examples below use
/tmp/weir-quickstart/wab.
60-second run
# 1. Build the daemon (release mode; debug mode is ~20× slower).
cargo build --release -p weir-server
# 2. Create the directories the daemon needs.
mkdir -p /tmp/weir-quickstart/wab /tmp/weir-quickstart/run
chmod 0700 /tmp/weir-quickstart/run
# 3. Run.
# --wab-segment-max-age-secs 2 is the low-volume default you almost
# certainly want (see the note right below): with the stock 256 MiB
# segment cap and no idle-seal, a handful of small records are acked but
# never sealed — so the sink (and any downstream you wire up) sees NOTHING
# until you stop the daemon. A 2-second idle seal drains quiet segments
# promptly. Drop the flag on a high-volume deployment where segments fill
# on their own.
./target/release/weir-server \
--wab-dir /tmp/weir-quickstart/wab \
--socket-path /tmp/weir-quickstart/run/weir.sock \
--wab-segment-max-age-secs 2
Why the idle-seal flag is here. Idle seal (
wab_segment_max_age_secs, CLI--wab-segment-max-age-secs) defaults to0(disabled): a segment seals only when it reacheswab_segment_max_bytes(default 256 MiB) or the daemon shuts down. A low-volume producer that writes a few small records and goes quiet gets acked but never seals — so the drain never forwards them and the sink receives nothing until shutdown. Setting it to a couple seconds (here2) makes a quiet segment seal on a timer and drain promptly. This is a first-class knob for any low-throughput deployment; details in configuration.md.
You should see a few INFO startup lines ending with the socket and the metrics
endpoint coming up — the two stable signals that it's ready:
INFO weir_server::socket: socket listening path=/tmp/weir-quickstart/run/weir.sock
INFO weir_server::metrics::server: metrics endpoint bound addr=127.0.0.1:9185
(Exact wording varies by version. You may also see benign WARN lines at
startup — e.g. a CPU-affinity note, or on macOS a durability note that
F_BARRIERFSYNC is not power-loss-durable; run production on Linux. workers
defaults to shard_count.)
That's a fully-functional weir daemon. Leave it running and move on to pushing a record.
Push your first record
Fastest path — weir-ctl push. If you built weir-ctl (it's in the
workspace), one already-compiled command pushes a record and prints the ack —
nothing to wire up:
weir-ctl push --socket /tmp/weir-quickstart/run/weir.sock 'hello, weir!'
# ack 12 bytes, Durable
A small load — the bundled example. From the weir checkout, in a second
terminal, run the ready-made push_simple example against the socket from the
run above:
cargo run --release -p weir-client --example push_simple -- \
--socket /tmp/weir-quickstart/run/weir.sock --count 5
push_simplepushes--countrecords at each durability tier (Durable, Buffered), so--count 5sends 10 records in total.
Before you build a producer — three gotchas that compile and pass a smoke test, then bite under load:
push()is blocking — never call it directly from anasync fn. It blocks the executor thread and starves the runtime (a multi-thread runtime masks it; a single-threaded one freezes). Use the bridge in Integrating → Producing from an async runtime.- Ack ≠ delivered. A successful
pushmeans durably buffered, not delivered to your sink. Nothing reaches the sink until a WAB segment seals, and the defaultnoopsink discards everything. For low volume, setwab_segment_max_age_secsso idle segments seal on a timer; configure a realsink_typeto actually deliver. Details in the Ack ≠ delivered note below.Bufferedis not power-loss durable. It acks before fsync. A process crash survives (page cache), but power loss / OS crash is a loss window — useDurablefor data you can't lose. (macOS is not power-safe at any tier — its WAB writes useF_BARRIERFSYNC, which orders writes without forcing the drive's cache to the medium; see Platform support.)
From your own project. When you're ready to push from your own code,
add just weir-client and write a small program against the synchronous
client API. Durability is re-exported from weir-client, so the basic
producer path needs only this one dependency — pull in weir-core
separately only if you need the lower-level wire types directly.
[dependencies]
weir-client = "4.0"
# Add `features = ["tls"]` for the TCP + mutual-TLS transport, which also
# builds on Windows.
# weir-core is only needed for the lower-level wire types, not a basic producer.
Then write the program against the synchronous client API:
// examples/hello.rs use weir_client::{Durability, WeirClient}; // Durability is re-exported here fn main() -> Result<(), Box<dyn std::error::Error>> { let mut client = WeirClient::connect("/tmp/weir-quickstart/run/weir.sock")?; // push() returns Result<(), ClientError>: Ok(()) means the record was // durably acked at the requested tier. There is no ack payload — no // server-assigned offset or id comes back. A dropped/ignored result hides a // failed push, so `push` is #[must_use]; handle it (here via `?`). let payload = b"hello, weir!"; client.push(payload, Durability::Durable)?; // Durable = fsync before ack println!("pushed {} bytes", payload.len()); // Verify the daemon is healthy. client.health_check()?; println!("health check ok"); Ok(()) }
The client API is synchronous — one blocking socket round-trip per call, so you
don't need an async runtime to use it. (If you're calling from inside one,
push() blocks the executor thread — see
Integrating → Producing from an async runtime
for the bridge pattern.) Add weir-client to your example/dev deps, then:
cargo run --release --example hello
You should see:
pushed 12 bytes
health check ok
In the daemon's terminal, you'll see the WAB receive the record (with
log_level = "debug" you'd see per-record activity; at info the
record passes through silently — which is the production default).
Verify the metrics endpoint
curl http://localhost:9185/metrics | head -30
You'll see weir's Prometheus metric families (the full catalogue is in monitoring.md):
# HELP weir_records_accepted Total records accepted from producers, by durability tier
# TYPE weir_records_accepted counter
weir_records_accepted_total{tier="durable"} 1
# HELP weir_records_ack Total records acknowledged to producers, by durability tier
# TYPE weir_records_ack counter
weir_records_ack_total{tier="durable"} 1
...
The exporter (prometheus-client) appends the _total suffix only to the
counter data rows — the # HELP/# TYPE rows carry the base name. A labeled
counter series is also absent until the first matching event initialises it, so a
family with no traffic yet shows only its # HELP/# TYPE lines and no data row.
The full metrics catalogue — every family, its labels, and the alerts that matter — lives in monitoring.md.
What just happened
When you called client.push(...):
- The client opened the Unix socket and sent a 16-byte header + your 12-byte payload + a 4-byte CRC32.
- The daemon validated the header (magic → version → CRC → payload cap), allocated a 12-byte buffer, read the payload, verified the payload CRC.
- The record was pushed onto the bounded queue. A worker thread picked it up and routed it to shard 0's flusher.
- The shard's flusher accumulated it into a batch. Because you asked
for
Durabledurability, the batch was fsynced immediately (no waiting for siblings). - The daemon sent the Ack frame back. Your client returned.
- The drain thread later read the sealed WAB segment, committed it
via the configured sink (the built-in no-op sink by default, which
accepts everything), and wrote a
.confirmedsidecar so the segment can be safely deleted.
For the full pipeline detail, see architecture.md.
Ack ≠ delivered. A successful
pushmeans the record is durably buffered (fsynced forDurable), not that it has reached the sink. The drain forwards records only after a WAB segment seals — at its size threshold (segment_max_bytes, default 256 MiB) or on shutdown. So with a handful of small records the sink isn't touched until you stop the daemon, and with the defaultnoopsink nothing is forwarded at all (it's a soak-test sink). Confirm acceptance withweir_records_ack_total, not the sink-commit metric. To make a low-volume deployment drain promptly, setwab_segment_max_age_secs(e.g.2) so an idle segment seals on a timer instead of waiting to fill 256 MiB; for a one-off demo you can also just sendSIGTERM(graceful shutdown seals the open segment).
Cleaning up
# Stop the daemon: Ctrl-C in its terminal, or kill the specific PID you started:
kill "$(pgrep -f -- '--wab-dir /tmp/weir-quickstart')"
# (This matches only the quickstart daemon by its --wab-dir flag. Avoid
# `pkill weir-server` on a shared host — it kills every weir daemon, not
# just this quickstart's.)
# Remove the quickstart data.
rm -rf /tmp/weir-quickstart
Common issues
bind: address already in use
A stale socket from a previous run wasn't cleaned up. Either delete
it manually (rm /tmp/weir-quickstart/run/weir.sock) or, more
typically, the previous daemon is still running. Check with
pgrep weir-server.
refusing to remove non-socket file at <path>
A regular file (or symlink) exists at the socket path. The daemon
refuses to remove it as a safety check — manually inspect and
remove if it's safe to do so.
invalid path for 'wab_dir': cannot canonicalize '…': No such file or directory
weir does not create the WAB directory (Postgres model). Create it
with mkdir -p before starting the daemon.
Metrics endpoint not reachable from another container
The metrics server binds to 127.0.0.1:9185 by default (localhost
only). To expose it, set metrics_bind = "0.0.0.0" (CLI
--metrics-bind, env WEIR_METRICS_BIND) and map the port in Docker:
-p 127.0.0.1:9185:9185. A 0.0.0.0 bind is not a security
boundary — control access via the port mapping or firewall.
Nack(PayloadTooLarge)
Your record exceeded the configured max_payload_bytes (default 16
MiB). Either raise the config value (up to the 16 MiB hard cap) or
shrink the payload.
Next steps
- Integrating & extending — use weir's crates without the daemon: produce from your own app, write a custom sink, or read the WAB directly.
- Configuration reference — every option, default, range, and when to tune.
- Install options — building from source, container
images, Docker Compose,
cargo install, and pre-built release binaries. - Wire protocol — frame layout if you're writing a non-Rust client.
- Security threat model — what weir defends against and what is the operator's responsibility.