rkat-rpc download provide ready-to-run installs.
This guide covers source builds, Rust embedding, container packaging and
release-binary platform requirements. The build measurements
help size a build host and choose compilation settings.
What A Build Compiles
The workspace has ~50 member crates. Its lockfile resolves ~670 package entries (~600 distinct crates) for the whole workspace with every feature, dev-dependency, and optional backend enabled. A default-featurecargo install rkat compiles ~380 crates.
Most of those are ordinary. The cost is concentrated in a few very large
first-party crates, each of which is one rustc and LLVM invocation:
These four, plus the final
rkat crate that pulls the whole surface into one
binary, dominate both wall time and peak memory. The optional consumer
profiles below therefore focus on job limits and per-package opt-level
pins rather than only the dependency count; they are not the current Windows
release recipe.
Toolchain and native requirements:
- The repository pins Rust
1.97.0inrust-toolchain.toml;make install-build-depsinstalls exactly that toolchain into a checkout. The published crates declarerust-version = "1.97.0", socargo install rkatneeds Rust 1.97 or newer. - The default feature set needs a C compiler and libc headers (bundled SQLite
and a few other C sources compile through the
cccrate) and nothing else. TLS isrustls; no OpenSSL development package is required and the release gate rejects binaries that link one. cmakeis needed only by the optionallive-webrtcfeature of themeerkatfacade andmeerkat-rpccrates (meerkat-livewebrtc, bundled Opus throughaudiopus_sys).rkathas no such feature, and no shipped binary enables it by default, so acargo installof any of them never compiles it.
Build Cost
Two cold builds, measured for this page:opt-level = 1 globally,
opt-level = 0 for meerkat-machine-schema), with the same job count so the
per-crate peaks are comparable. Peaks were sampled once per second from ps.
How to translate that to your host:
- The largest single crate is the floor, and only
opt-level = 0moves it. Even withCARGO_BUILD_JOBS=1, the build needs as much memory as its biggestrustcprocess. In both runs above that wasmeerkat-mobat ~11 GiB: going fromopt-level = 3to1halved the aggregate peak, therkatcrate, andmeerkat-machine-schema, but leftmeerkat-mobwhere it was. A third run (the samerkatbuild with the low-memory profile plusmeerkat-mobpinned toopt-level = 0) broughtmeerkat-mobdown to ~6.3 GiB, after which the largest process ismeerkat-runtimeat ~7.4 GiB. Below that, the levers are the feature set (use--no-default-featuresand omitmob,rpc-surface, and the CLIopenai-livefeature to avoid their mob dependencies in a reduced production build), swap, and prebuilt binaries. See Features for scope and limits. - Aggregate memory scales with the job count. Cargo runs up to one
rustcper core. The aggregate peaks above happened while four or five large first-party crates were compiling at once; withNjobs, budget for theNlargest crates that can be in flight together.meerkat-mob,meerkat-runtime, andmeerkat-machine-schemadid overlap in the default build, and that overlap produced its peak. - Wall time scales with cores only up to the serial tail. The hundreds of
small dependencies compile in parallel in the first few minutes, but the
large first-party crates depend on each other, and the final
rkatcrate compiled alone for roughly the last third of the wall time. A 4-core cloud VM should expect a few times the wall time above, and the tail does not shrink with more cores.cargo installalso has no warm cache to reuse, so every install is a cold build. - Disk is transient for
cargo install. The temporary target directory is deleted after the binary is installed, but it must fit while the build runs. A checkout that keeps itstarget/needs the full figure per profile.
.github/workflows/release.yml, build_binaries_windows_cross runs on
ubuntu-24.04 and invokes .github/actions/build-windows-cross/action.yml.
That action uses cargo-xwin with clang-cl, lld-link, and the Windows SDK to
cross-compile the four surface binaries and package their ZIP archives. It
adds an 8 GiB swap file when total swap is below 8 GiB; it does not set
CARGO_BUILD_JOBS=2 or inject the per-package optimization pins shown below.
The subsequent verify_windows_binaries job runs on windows-2025-vs2026,
extracts the archives, executes each binary’s --version, checks
rkat --help, and checks the SQLite lock profile rkat storage doctor --json
reports (see SQLite Lock Profile); it does not compile
them natively. These workflow settings are
not new wall-time or memory measurements. Continuous integration
(.github/workflows/ci.yml) uses BuildBuddy for its broad lane and hosted
Linux for the required dense Mob topology stress; release binaries are
produced by the separate release workflow.
Rough sizing, derived from the peaks above:
Lowering Memory
Where profile settings come from
Cargo reads[profile.*] tables only from the root manifest of the build it
is running. Two consequences matter here:
- The Meerkat workspace ships no
[profile.release], so its own release builds run at Cargo’s defaults (opt-level = 3,codegen-units = 16,lto = false,debug = false) except where a lane injects pins. - Even if it did, that would not reach you.
cargo install rkatunpacks the publishedrkatcrate as the root of its own temporary workspace; the repository’s rootCargo.tomlis not part of that crate. A downstream binary that depends onmeerkat = "=0.8.32"is likewise governed by its own root manifest, never by Meerkat’s.
cargo install honors Cargo configuration
from a .cargo/config.toml in the directory you run it from (or
~/.cargo/config.toml), from CARGO_PROFILE_RELEASE_* environment variables,
and from --config on the command line. This was verified for this page with
cargo install -v: with CARGO_PROFILE_RELEASE_OPT_LEVEL=1 set, rustc
receives -C opt-level=1. Library embedders put the same tables in their own
root Cargo.toml.
Low-memory profile
meerkat-mob (~11.5 GiB at opt-level = 1, ~6.3 GiB
at 0); it trades CPU on the orchestration path for memory, so measure your
workload before shipping a binary built this way:
opt-level line and keeping these pins.
This is a consumer profile choice, not the current Windows release recipe:
--config; repeat the flag with
profile.release.package.meerkat-mob.opt-level=0 for the small-host pin.
What each lever does
CARGO_BUILD_JOBS(or-j) caps the number of concurrentrustcprocesses. Aggregate memory is the sum of the compilers running at once, so this is the first lever on any memory-limited host. Wall time rises roughly in proportion.opt-level: going from3to1removes most of LLVM’s expensive passes. Measured on the crates whose peak is in LLVM, it halved therkatcrate (~10.1 to ~4.8 GiB) and, together with theopt-level = 0pin,meerkat-machine-schema(~9.4 to ~4.8 GiB), and it cut the whole build’s wall time from ~25 min 29 s to ~19 min 6 s. It did not movemeerkat-mob; onlyopt-level = 0did (~11.5 to ~6.3 GiB). The runtime cost of1is modest for an agent host that spends its time waiting on network I/O.opt-level = 0is free formeerkat-machine-schemabecause it is a generated catalog, not hot code; onmeerkat-mobit is a real trade.codegen-units: LLVM optimizes a crate’s codegen units in parallel, bounded by your core count, so a crate split into 16 units can hold 16 LLVM contexts at once. Lowering it reduces that per-crate parallelism at the cost of a longer compile for that crate, andcodegen-units = 1also widens inlining scope, which can raise memory for the largest crates. Measure on your host before changing it; the release lane leaves it at 16.lto: leave it at the defaultfalse."thin"or"fat"LTO pulls the whole dependency graph into one optimization pass at link time and multiplies link-stage memory.- Features:
crates/meerkat-cli/Cargo.tomllists the subsystems behindrkat’s default features (mob,rpc-surface,openai-realtime,comms,schedule,workgraph,mcp,skills, storage backends, providers). A narrower--no-default-features --features ...production build can remove the mob graph only if it omitsmoband all transitive enablers, includingrpc-surface(which selectsmeerkat-rpc/mob) and the CLIopenai-livefeature (which selectsmob). Omitting just the directmobfeature while keepingrpc-surfacestill buildsmeerkat-mobandmeerkat-mob-mcp. The repository’s surface feature matrix (scripts/run-surface-feature-matrix, behindmake test-feature-matrix-surface) keeps three reducedrkatcombinations compiling:--no-default-features --features session-store,session-store,mcp, andsession-store,comms,mcp,workgraph. Other subsets are not exercised, so check that yours builds before relying on it. Reduced builds are source builds only; no reduced binaries are published (see the note under Homebrew Tap). This reduction is for the selected production package: workspace/test feature unification and dev-dependencies can bring mob back, and removing it is not a guarantee that every host below 8 GiB can compile the remaining graph.
Worker Stack Budget
Every host binary (rkat, rkat-rpc, rkat-rest, rkat-mcp,
rkat-mob-mcp) runs its Tokio workers, blocking threads, and its own main
future on one documented stack budget: 8 MiB per thread, the constant
meerkat_runtime::host_stack::HOST_WORKER_STACK_BUDGET. The main future runs
on a budgeted thread as well, so the platform main thread (1 MiB on Windows)
never bounds a host.
The number is measured, not guessed. On the RPC router harness (session
create, turn start, session archive through the real method router) a release
build fits 1 MiB and overflows at 512 KiB; a debug build, which the e2e lanes
run, fits 4 MiB and overflows at 3 MiB because opt-level 0 gives every branch
of a function its own stack slots. 8 MiB is twice the debug figure and eight
times the release one. It is virtual address space per thread; pages are
committed only when touched. Two gates hold the figures: a debug canary in the
meerkat-rpc unit lane at 4 MiB and the nightly make stack-budget-release at
1 MiB in release.
RKAT_WORKER_STACK_BYTES=<bytes> overrides the budget for stack-overflow
diagnosis. Sweeping it distinguishes a bounded large frame (fails below one
value, passes above) from unbounded recursion (fails at every value). Values
below 65536 or that are not a byte count are refused with exit code 2 rather
than guessed at. rkat-rpc still honours its older RKAT_RPC_WORKER_STACK_BYTES
when the shared variable is unset.
Meerkat also keeps two small Tokio runtimes of its own for the whole process,
in a host binary and in a Rust embedding alike: the machine cleanup runtime
(threads named meerkat-machine-cleanup) and the runtime-loop serving runtime
(meerkat-runtime-serving). They run Core’s own session and mob work: machine
cleanup, the custody of a session’s creation and recovery, and the runtime
loops that apply each session’s turns. The turns themselves, with the host’s
tools and whatever those spawn, run on the host’s runtime: a mob member’s
turns on the runtime of the task that provisions it, the turns of a session an
RPC session runtime serves on the runtime that session runtime is built on,
and those of a session the host creates directly on the runtime that creates
it. Size the workers of that runtime for the tools you compose. A host runtime
that stops in the middle of a turn ends the turn as a crash would: the
interrupted input and the queued ones stay non-terminal, nothing more is
written for them, and the session’s next start resumes them. An input that
reaches an idle session after the stop waits for that start too. Once the
session’s runtime takes such an input up, as after a stop in the middle of a
turn, the session’s registration waits for its reload, and a later input is
refused until the next start, with nothing kept for it; an RPC session runtime
refuses it with host_runtime_stopped (two inputs to the same session within
milliseconds of each other right after the stop can see the second refused
untyped instead, -32603, with nothing kept). On an RPC session runtime two
more inputs are refused instead of waiting: a governed input, and an input to a
help session, which is never rebuilt from storage and is refused with
help_session_not_resumable. A turn whose task
panicked, or whose session the service retired, fails with its error, as does
a turn its caller drives directly. The registration such a stop leaves waiting
for its reload stays in its machine until that machine reloads it, at the
session’s next registration there, or the process exits. With realm
persistence, whose hosting claims are file locks, it keeps the session’s lock
until then, so any other machine, in this process or in another, is refused
the session (ServedElsewhere); the stopped host’s session service and
machine also stay in the process until it exits. When RKAT_WORKER_STACK_BYTES is unset the
two process runtimes’ threads take 16 MiB, twice the budget, or
RUST_MIN_STACK when that asks for more. They follow the same variable as the
host: RKAT_WORKER_STACK_BYTES sets their thread stack exactly, below 16 MiB
included, so the diagnosis sweep reaches them too. Their threads have an
explicit size, so Rust itself would never apply RUST_MIN_STACK to them; this
way an embedding whose workers or debug build need more than 16 MiB raises
these threads with the same RUST_MIN_STACK it gives its own. An invalid
RKAT_WORKER_STACK_BYTES fails every operation that needs one of these
runtimes until it is corrected, with the refusal a host binary gives at
startup. RKAT_RPC_WORKER_STACK_BYTES does not reach them.
Worker Threads
A host binary’s Tokio runtime starts one worker thread per available CPU, asstd::thread::available_parallelism counts them: a cpuset or a cgroup CPU
quota (docker run --cpus, a Kubernetes CPU limit) lowers the count, a pids
limit does not. RKAT_WORKER_THREADS=<n> sets the count for every host
binary; when it is unset, Tokio’s own TOKIO_WORKER_THREADS is read in its
place. A value that is not a whole number of at least 1 is refused with exit
code 2 rather than guessed at. Two small internal runtimes (the runtime-loop
serving runtime and the machine cleanup runtime) always run two workers each.
A Rust host that builds its own Tokio runtime can apply the same setting:
meerkat_runtime::host_stack::worker_threads_from_env() returns the validated
count, or None for the default.
SQLite Lock Profile
Meerkat’s stores run on the SQLite thatlibsqlite3-sys bundles. Three
defaults of that build put one process-wide mutex in front of every
connection in the process, so sessions that share a host process queue on
each other:
On a shared 192-core host (
meerkat-sqlite’s process_wide_locks_bench
test measures this), sixteen threads that each opened their own database
opened fewer connections per second together with the defaults (about 700)
than one thread alone (about 3,000), and 10,000 to 24,000 with allocator
statistics off. Sixteen threads writing to their own databases ran about six
times faster with one page cache per connection. The overflow statistics lock
is taken least often: turning it off as well made no difference beyond the
host’s run-to-run variation.
One build setting turns all three off
(meerkat_sqlite::REQUIRED_LIBSQLITE3_FLAGS):
sqlite3_release_memory). The
repository sets the value for every build it makes, Cargo through
.cargo/config.toml and Bazel through MODULE.bazel, so the release binaries
carry it. The release workflow runs rkat storage doctor --json on each
rkat it builds where the runner can execute it, and fails when a lock is on:
every target of the GitHub-hosted lane, the x86_64 Linux target of the
BuildBuddy lane (its other targets build from the same Bazel annotation),
Intel macOS under Rosetta, and Windows on the Windows verification runner.
A build outside the repository does not read those files: set the variable
yourself for cargo install rkat and for an application that embeds Meerkat.
Without it, every host binary still turns allocator statistics off as the
first statement of main, but the shared page cache and the overflow
statistics stay on.
rkat storage doctor reports the profile its binary runs with (the
sqlite_lock_profile object of --json, and a line of the text report) and
warns with the finding sqlite-lock-profile when a lock is on. A process
that opens its first store connection with a lock on also logs a warning that
names the variable.
A Rust application that embeds Meerkat sets the variable for its own build,
and calls meerkat_sqlite::disable_memory_statistics_at_process_start() as
the first statement of its main. The function is unsafe: SQLite accepts
the switch only before anything uses it, and no other thread may use SQLite
while it runs. meerkat_sqlite::check_lock_profile() and
meerkat_sqlite::SqliteLockProfile::effective() tell the application which
locks it runs with.
Containerizing The Binaries
A Rust binary built for a*-unknown-linux-gnu target links the builder’s
glibc dynamically and records the newest glibc symbol version it used. The
runtime image must provide a glibc at least that new, or the loader refuses to
start the binary with version 'GLIBC_2.xx' not found. The reliable way to
get that right in a two-stage image is to pin one Debian release for both
stages, so the builder’s glibc is the runtime’s glibc:
ARG DEBIAN_RELEASEis declared before the firstFROMand used in bothFROMlines, so the two stages cannot drift apart when someone bumps one of them.rust:1.97-bookwormmatches the pinned toolchain family.--lockedbuilds with theCargo.lockshipped inside therkatcrate, so the container compiles the dependency set the release was tested with.- To build from a checkout instead of crates.io (unreleased commits, local
patches), replace the
RUN cargo installline withWORKDIR /src,COPY . ., andcargo build --locked -p rkat --release, and copy/src/target/release/rkat(the build stage above sets noWORKDIR, so without that line the checkout lands in/). The repository’s ownexamples/035-mdm-tux-rs/Dockerfiledoes exactly that for the example’s binaries, with the samerust:1.97-bookwormbuild stage anddebian:bookworm-slimruntime stage. - The companion surfaces are separate crates with the same shape:
cargo install meerkat-rpc(binaryrkat-rpc),meerkat-rest(rkat-rest), andmeerkat-mcp-server(rkat-mcp). rkatresolves project realm state under.rkat/in its working directory and the global realm’s config under~/.rkat/(see Configuration); mount a volume at/data, and pointHOMEat persisted storage if the global realm matters, so state outlives the container.
debian:bookworm-slim ships glibc 2.36, above
the 2.31 floor the release binaries are built against (next section):
checksums.sha256 (and, if you use
the GitHub CLI, its build provenance attestation; see below) before trusting it
in production.
Not covered by the prebuilt binaries: Alpine and other musl-based images. No
musl target is published, and the glibc binaries are not tested under
gcompat. Build from source on such a base or use a Debian-family image.
Thread and process limits
A container’s pids limit (docker run --pids-limit, the cgroup’s pids.max,
the kubelet’s pod PID limit) counts threads as well as processes, and so does
RLIMIT_NPROC (ulimit -u) across all of a user’s processes. A host started
on a machine with more CPUs than its limit allows cannot start one worker per
CPU (see Worker Threads), and fails at startup:
(os error 11). Set the
worker count well below the limit, for example
docker run --pids-limit 256 -e RKAT_WORKER_THREADS=8 ...; on releases before
0.9.3, TOKIO_WORKER_THREADS=8 has the same effect.
Size the limit for everything the host runs, not only its workers:
- An idle
rkat-rpcruns about ten threads beyond its workers: its main threads, the stdin reader, a file watcher, scheduler and delivery threads, and a few blocking-pool threads. - Each live session with a persistent store adds one thread.
- Blocking work (file and SQLite I/O, DNS lookups) runs on Tokio’s blocking
pool, which can grow to 512 threads per runtime under load; idle ones exit
after 10 seconds. A blocking-pool spawn the limit refuses is queued until
one of the pool’s threads is free, but only while the pool has a thread:
in a pool with none, Tokio panics instead (
OS can't spawn worker thread). - Every MCP stdio server and shell command is a process inside the same limit, with threads of its own.
hint: line naming the usual causes and RKAT_WORKER_THREADS;
a library that does not handle the refusal can abort the process instead.
Leave headroom rather than sizing the limit to the worker count.
Since 0.9.4 one such library failure no longer aborts the host: when a SQLite
change watch registers its file notifications and notify’s inotify event-loop
thread cannot start, the watch keeps running on its bounded sweep alone (every
5 seconds by default) and logs “sqlite change watch notifications are
unavailable; the watch runs on its sweep alone”. The same happens at an inotify
instance or watch limit; raise fs.inotify.max_user_instances or
fs.inotify.max_user_watches on the host and restart to get notifications
back. Each watch registers on a thread of its own that ends once the platform
has registered (at once with inotify), and at most 64 registrations run at once
in a process.
Where The Prebuilt Binaries Come From
.github/workflows/release.yml builds and publishes the release assets when a
v* tag is pushed. Each of the four binaries (rkat, rkat-rpc, rkat-rest,
rkat-mcp) is packaged per target as <binary>-<version>-<target>.tar.gz
(.zip on Windows), alongside checksums.sha256 and index.json. The
Homebrew tap and the
SDK bootstrap consume these same
assets. Targets:
x86_64-unknown-linux-gnuaarch64-unknown-linux-gnuaarch64-apple-darwinx86_64-apple-darwinx86_64-pc-windows-msvc
actions/attest-build-provenance), which binds the file’s digest to the
workflow run and commit that produced it:
Linux glibc floor
The Linux GNU binaries are built inside thebuildpack-deps:bullseye image
(Debian 11, glibc 2.31) on both release lanes: the BuildBuddy lane pins that
image as the execution container in tools/bazel/platforms/BUILD.bazel, and the
GitHub-hosted lane runs its Linux jobs under the same container:, because
building directly on the Ubuntu 24.04 runner (glibc 2.39) would silently raise
the floor. Packaging then runs
scripts/check-linux-release-binary-portability.sh on every Linux binary and
fails the release if either check trips:
- Any versioned glibc symbol reference is newer than the declared floor
(
MEERKAT_GLIBC_FLOOR=2.31). - The binary has a dynamic
NEEDEDentry onlibsslorlibcrypto. All first-party TLS isrustls, so an OpenSSL dependency means a crate regressed into a native-TLS stack.
GLIBC_2.34 that
Debian 11 could not load; the floor is enforced on the produced file, not
assumed from the build image.
What that guarantees: a published Linux binary starts on any x86_64 or aarch64
distribution whose glibc is 2.31 or newer (Debian 11 and later, Ubuntu 20.04
and later, Debian-based container images from bullseye onward) and needs no
OpenSSL runtime package. What it does not guarantee: musl-based systems (see
above), or distributions older than glibc 2.31.
Other platforms
The macOS binaries carry no notarization and the release workflow asserts no minimum macOS version (there is no deployment-target pin or gate); the GitHub-hosted lane ad-hoc codesigns them. The Windows binary is x86_64 only. The packaging step checks that every binary embeds the release version, sorkat --version on an installed binary tells you exactly which release it is.
Meta
Meta’s gateway binaries are built and released by the meerkat-meta repository’s own lane; the glibc floor and portability gate described on this page do not apply to them. Start from the Meta quickstart for its install and build paths.See Also
- CD and distribution for the release pipeline, Homebrew tap, SDK bootstrap, and credentials.
- Build And CI for the contributor build, test, and BuildBuddy lanes.
- Quickstart for the featured install paths.
- Upgrading for per-release upgrade steps, including shutdown deadlines and termination grace periods.
