Footprint & performance
Every number on this page was measured, not estimated. Anything reasoned rather than measured is labelled as such.
The short answer
| Floor | 64 MB of RAM. Verified: 48 MB is killed before ready, 64 MB reaches it |
| Comfortable | 512 MB. 1 GB if the agent does real work through exec |
| Disk | ~95 MB installed, plus the install. A fresh data/ is 364 KB |
| CPU | Any. 0.7% of one core at idle — it waits on the network almost all the time |
| Needs no | Postgres, Redis, vector database, GPU, or local model |
There is no minimum-viable-model requirement, because Nouride does not host one. Providers are remote endpoints, so RAM is unaffected by which model an agent uses.
Idle
The daemon at rest, gateways connected, no turn in flight.
| Shape | RSS idle | Peak during boot | Artifact |
|---|---|---|---|
| Compiled binary, Linux, 1 agent | 74.5 MB | 139.7 MB | 95 MB installed |
| Compiled binary, macOS, 0 agents | 63.7 MB | — | 58 MB |
| Docker image | 69 MB process / 96 MB container | 166 MB | 85 MB image |
Sampled every 15 s for a minute: flat, no drift, 12 threads, no child processes.
The boot peak is roughly double the resting figure — module loading and migrations — and it is what a memory limit has to accommodate, not the resting number. A limit below it produces an OOM kill during startup, which reads as a crash loop with no error in the daemon’s own log: the process never gets far enough to write one.
The binary and the image land within ~10 MB of each other, so do not choose between them on memory grounds. Choose on whether the agent should be able to act on the host.
The floor
Run under a hard cgroup limit, checking whether the daemon reaches ready.
| Limit | Result | RSS |
|---|---|---|
| 256 MB | ready | 75.4 MB |
| 128 MB | ready | 55.6 MB |
| 96 MB | ready | 46.7 MB |
| 64 MB | ready | 24.2 MB |
| 48 MB | killed before ready | — |
The runtime adapts to the limit rather than demanding a fixed budget. Given 64 MB it runs the heap in 24 MB; given 256 MB it spreads out to 75 MB. So “it needs 140 MB” is the wrong reading of the boot peak.
Do not deploy at the floor. It is the number that proves the daemon is small, not a recommendation — at 64 MB there is nothing left for a turn.
Agents are almost free
| Agent packs loaded | RSS | Boot peak |
|---|---|---|
| 1 | 74.4 MB | 140.5 MB |
| 3 | 74.7 MB | 140.4 MB |
| 6 | 76.5 MB | 140.4 MB |
Roughly 0.4 MB per idle agent. An agent is a directory of Markdown, not a process: the prompt is assembled per turn and not held resident, and every agent shares one process, one database and one HTTP server.
This is the number to size against when adding agents. It is the wrong number for sizing concurrent work.
A turn under load
Not measured — it needs a live provider key and a real conversation. What bounds it is configuration, so the ceiling is at least knowable:
| Setting | Default | Why it drives memory |
|---|---|---|
tool_max_output_bytes | 10 MB | One tool call’s output, held to be returned to the model |
max_tool_iterations | 25 | Tool calls in a single turn |
max_conversation_turns | 200 | Stored history per conversation |
max_context_ratio | 0.8 | Share of the model’s context window that gets filled |
max_tokens_per_turn | 16384 | Response size |
Memory scales with concurrent turns, not with agents. The queue is ordered per conversation and parallel across conversations, so ten chats answered at once is ten contexts assembled at once. One agent in ten busy channels costs more than ten agents in one quiet one.
Reasoning, not measurement: a single turn reading a large file or running an exec that prints
megabytes is the realistic worst case. That is why the recommendation is 512 MB rather than 128 MB.
--smol
The released binary is compiled with --smol baked into its execArgv, so it runs a smaller heap and
collects more often. Measured on two live boxes, before and after the same build otherwise:
| RSS before | RSS after | heap before | after | |
|---|---|---|---|---|
| 6 agents, 9 gateways | 76.5 MB | 65.6 MB | 48.6 MB | 41.4 MB |
| 1 agent, 2 gateways | 95.4 MB | 79.3 MB | 46.4 MB | 33.2 MB |
The cost is CPU: an allocation-heavy benchmark shaped like a turn runs 21% slower. That is the right
trade here, because a turn’s wall time is dominated by waiting on a model rather than by local
allocation — but bun run build:binary --no-smol builds without it for a host with memory to spare.
Build variants
Every combination, measured on one linux-x64 box by the same method.
| Build | Binary | Tarball | RSS idle | Licence |
|---|---|---|---|---|
| (default) | 80.3 MB | 74.7 MB | 51.8 MB | — |
--no-wa-bridge | 80.3 MB | 38.4 MB | 51.8 MB | — |
--bundle-wa-bridge | 95.0 MB | 43.5 MB | 58.0 MB | GPL-3.0 |
--bundle-nougate | 80.7 MB | 74.8 MB | 53.0 MB | — |
| both bundles | 95.3 MB | 43.6 MB | 59.7 MB | GPL-3.0 |
RSS idle here is the daemon carrying the code with the feature switched off — what it costs to
have the option.
--no-wa-bridge produces a byte-identical binary to the default, same sha256. The 36 MB it saves
is the bridge executable dropped from the tarball, not anything in the daemon.
A bundle you do not switch on is not free. The dynamic import means the code is never evaluated, but a bundled module graph is registered either way — which is why the non-bundle builds externalise it rather than relying on the import alone.
Separate process versus bundled
| Component | Its own process | Bundled | Saved |
|---|---|---|---|
| WhatsApp bridge | 62.2 MB | +21.3 MB | 40.9 MB |
| LLM gateway (nougate) | 44.6 MB | +6.0 MB | 38.6 MB |
| both | 106.8 MB | +27.3 MB | 79.5 MB |
Why the saving is roughly 36 MB either way: a separate process cannot share the Bun runtime. Measured by running one binary twice, ~36 MB of runtime and bundle pages are shared between processes of the same executable, and ~14.5 MB of heap is private to each. So a second process pays the 36 MB again; a second listener or a bundled module pays none of it.
Both separate artefacts are almost entirely Bun: a bare compiled hello-world for linux-x64 is
78.7 MB, so nougate’s own code is 0.3 MB of its 79.0 MB.
Sizing a box
| Target | Verdict |
|---|---|
| Raspberry Pi 4 / 5 (1 GB+) | Comfortable. --target linux-arm64. A Pi 5 is more than this needs |
| Any 512 MB VPS | Fine, including the dashboard. The cheapest tier at most providers |
| 1 GB VPS | The recommendation. Headroom for exec output and several concurrent chats |
| Mini PC / NUC / old laptop | Overkill, and the right choice anyway if the agent does infra work |
| NAS with Docker | Fine. Check uname -m — several are arm64 |
| Alpine / musl | Needs the -musl build and apk add libstdc++ |
For Kubernetes, a starting point rather than a tuned answer:
resources: requests: memory: 128Mi # comfortably above the 64 MB floor cpu: 50m # 0.7% of a core at idle limits: memory: 512Mi # accommodates the boot peak and a turn with large tool output cpu: "1"Watch the actual peak before tightening limits.
The daemon is often not the largest process in its own cgroup
Worth knowing when planning a small box. On a live install, wa-bridge measured 67.8 MB and a single
Node-based MCP server 91.5 MB, against the daemon’s 73.3 MB.
On a 256 MB device the daemon and the bridge fit. Adding one Node MCP server does not.
Why it is this small
Not tuning. Four architectural choices:
- Bun/JSC rather than Node/V8. Under memory pressure the measured behaviour is to shrink to fit, rather than to hit a heap ceiling and abort.
- No external infrastructure. One SQLite file. The daemon links three third-party packages.
- No browser and no local model. These are what push comparable runtimes into gigabytes.
- Agents are configuration, not processes. 0.4 MB each, and that is a different category rather than a better implementation of the same one.
Part of the gap is scope. Adding browser automation would erase the measured advantage immediately: a Chromium instance is 300 MB to over 1 GB, four to fifteen times the entire daemon. It is not planned, and the resource profile is only the third reason — the first is that a browser driven by a model taking instructions from a chat message is a general-purpose request engine inside your network boundary, with no equivalent of the exec approval gate. If it is ever wanted, it belongs in an MCP server, out of process and behind a tool boundary that can be refused per agent.