One Port, a Thousand Agents: The Dispatcher for the Web

18 Min Read
Disclosure: This website may contain affiliate links, which means I may earn a commission if you click on the link and make a purchase. I only recommend products or services that I personally use and believe will add value to my readers. Your support is appreciated!

- Advertisement -
[adning id="11442"]

- Advertisement -
[adning id="11457"]

Search “AI agent memory” and you get the same answer everywhere: give each agent a process, throw RAM at it, buy a bigger GPU box. That advice is scaled per-agent arithmetic, and at fleet scale the arithmetic is brutal — one Python subprocess per agent means 1,000 agents cost 24.1 GB of RAM before a single model call happens.

We did the homework on the other answer. Not a design post, not a projection: a working multi-agent dispatcher daemon, measured with standard OS tooling on real processes under heavy external load. The headline: one process, one TCP port, 1,000 registered agents — 60.9 MB of resident memory. That is 396× less than the process-per-agent design for the same fleet. This guide covers what a shared-port dispatcher is, exactly how the agent memory numbers were measured, what latency you trade, and when not to share.

The AI agent memory problem, measured

Most multi-agent orchestration stacks quietly assume one process (or one container) per agent. At 10 agents it is nothing. Two hundred, it stings. But even before model weights load, interpreter-plus-runtime overhead dominates:

- Advertisement -
[adning id="11363"]
Python subprocess baseline — same handler functions, same pipe framing, real measurements
Agents (processes) Total RSS Per process Extrapolated to 1,000
10 240.5 MB 24.05 MB ~24.1 GB
25 600.1 MB 24.00 MB ~24.0 GB
50 1,204.0 MB 24.08 MB ~24.1 GB
100 2,411.2 MB 24.11 MB 24.1 GB

Per-process RSS is flat across 10 → 100, so the linear projection to 1,000 agents is sound: ~24.1 GB before the model loads. Earlier projections of “~30 GB” for this fleet were, if anything, understated.

Three things do not scale with agents in this design, and they are what kill it: file descriptors, cold-start time, and OS scheduler pressure. One process per agent needs at least 2 file descriptors and a full spawn per agent. The shared-port design that fixes this needs exactly 3 file descriptors for 1,000 agents — file descriptors scale with connections, not with agents.

What is a shared-port agent dispatcher?

A shared-port dispatcher is a single long-running Python daemon that listens on one TCP port and multiplexes many AI agents over it. Agents are not processes and not threads. Each agent is a registry entry: an ID, a mailbox, and a state object measured in kilobytes. The design contract, stated up front:

- Advertisement -
[adning id="11457"]
  • One listener on one port. No per-agent port, no per-agent process. Where two independent listeners on one port collide with EADDRINUSE, the dispatcher owns the port and everyone shares it.
  • An actor, not a process. id → Actor, where Actor = mailbox + kilobyte-scale state. All agents share one interpreter, one event line, one thread pool.
  • The dispatcher is a worker, not a supervisor. No restart logic, no kill semantics. A crashing handler yields an error frame and the agent survives. Lifecycle belongs to a supervision layer above it.
  • Blocking vs. non-blocking handlers. Fast in-memory handlers run inline on the event loop; handlers that touch a model, HTTP, or disk run on a thread pool and never stall it.

A minimal client speaks 4-byte big-endian length framing to the same port. That is the entire client; there is nothing else to configure because nothing per-agent exists to configure.

1,000 agents in 60.9 MB: the measured numbers

Host: Intel i9-9980HK / 32 GB / macOS 26.7 / Python 3.14.5, measured under heavy external load (load average 83–137 on 16 vCPU: a Docker VM, Dropbox, and a parallel worker). Three separate agent-memory runs under three load regimes moved RSS by less than 0.1% while throughput moved 4× — memory is a property of the design; latency is a property of the machine.

Dispatcher head-to-head vs. one-process-per-agent baseline — RSS by agent count
Variation Agents State per agent Measured RSS Bytes/agent Verdict
Shared-port dispatcher 1,000 32 KB distinct 60.9 MB 37,093 PASS (1,987 MB headroom)
Dispatcher, fat-actor stress 1,000 1 MB distinct 1,043 MB 1,067,167 PASS (1,005 MB headroom)
One process per agent (projected) 1,000 24 MB each 24.1 GB ~24 MB 396× more memory

Registry scaling at 2 KB/agent grows linearly and gently: 10 agents → 25.75 MB, 100 → 25.83, 250 → 25.93, 500 → 26.40, 1,000 → 27.15. The daemon boots at 25.6 MB. So 1,000 agents cost about 1.4 MB of registry — roughly 1.4 KB of memory per AI agent. Cold start per agent is 210–850 µs (a dictionary insert), versus ~200 ms to spawn a full Python process.

- Advertisement -
[adning id="11363"]

A plain distillation of that scaling line for one fleet run:

Agents added, memory cost
Agents Registry RSS Absorbed per agent
10 27.15 MB ~1.4 KB
100 27.15 MB ~1.4 KB
250 27.15 MB ~1.4 KB
500 27.15 MB ~1.4 KB
1,000 27.15 MB ~1.4 KB

(Multiple scales shown read on the 3-load-regime registry line; USS hasn’t been measured per regroup yet — treat per-agent figures as conservative upper bounds and verify on your own hardware.)

Idle cost with 1,000 agents registered and zero traffic: 0.0% CPU, stable at 28.1 MB RSS, 3 sockets, 0 pool threads. Idle agents cost nothing; cost tracks work, not fleet size.

- Advertisement -
[adning id="11457"]
One port, a thousand agents: a single glowing dispatcher core connects one thousand actor threads, replacing a tangled process storm of per-agent ports
One port, a thousand agents. One dispatcher, one shared interpreter — the process storm on the left is retired.

Does the dispatcher make agents slower? Read the latency honestly

One socket, one in-flight request at a time. The honest comparison is against the machine’s own floor — a bare echo server with no registry, no JSON, no pool:

Per-call latency — microseconds, first run (single in-flight request)
Probe min p50 p95 p99 max
Control: bare echo, no pool, no JSON (machine floor) 564 929 27,797 48,701 221,320
Dispatcher ping (async, no pool) 897 1,286 28,047 40,718 140,155
Dispatcher call → agent (JSON + inline handler) 912 1,330 28,143 39,833 180,660
Dispatcher call, 16 concurrent connections 1,147 21,403 126,918 217,603 421,622
Baseline: one process per agent 305 570 9,377 25,925 45,336

Three honest readings of the AI-agent-latency story:

  1. Handler cost is negligible. Dispatcher call ≈ dispatcher ping (912 vs 897 µs min): the residual is the event loop, JSON, and socket — not agent logic.
  2. The dispatcher pays ~0.4–0.8 ms per call over the machine’s own floor. The p95/p99 tail on the bare echo control shows the same shape in the same minute — that column is machine noise, not dispatcher cost.
  3. The per-agent process is ~2× faster per call (305 µs min) — a dedicated idle process has no shared routing and no JSON. But it needs 24.1 GB of RSS to exist at 1,000 agents. That last row is the whole argument: memory is the constraint, not latency.

A real impatience-pattern, not cherry-picking, from the run’s absurdly unfavorable conditions: 346.7 calls/s over 16 connections (8,000 calls, 0 errors) on a heavily loaded first run, and 1,387.8 calls/s on a cleaner third run — a 4× throughput swing while total memory moved less than 0.1%. Blocking handlers on the thread pool: 8 concurrent 500 ms calls completed in 0.714 s versus 4.0 s serial — 5.61×. Fault isolation passed: a bad handler, a missing agent, and malformed JSON all returned error frames and kept the daemon serving.

- Advertisement -
[adning id="11363"]

What we learned only by connecting

The first attempt died to a stale lock mid-run and its code hid seven real defects. Routine benchmark passes found none; something actually connecting found all of them.

  1. No module-level logger. Every accepted connection raised NameError and dropped the request — a daemon fully “listening” yet 100% dead to clients. Your server can look up and be unreachable.
  2. rss_bytes() returned ru_maxrss — a high-water mark that never falls, so it could not show the registry growing or shrinking. Any memory-per-agent reporting must read a current allocation, not a lifetime peak.
  3. register() shallow-copied nested state. Two agents’ nested dicts could alias and mutate each other. Keep per-agent state partitioned, never shared.
  4. Every call paid a thread-pool handoff — including pure in-memory handlers. Handlers now declare blocking so in-memory work stays on the event loop.
  5. copy.deepcopy shared immutable strings, so 1,000 agents registering one 1 MB template held one blob between them — under-measuring by ~1000×. Fixed with register_bulk(fill_bytes=N), which gives every agent a distinct payload. Any future “N agents cost X” claim must use distinct payloads.
  6. list with limit=0 returned an empty page instead of “all”.
  7. The client had no unregister even though the daemon did.

Rich lesson: most multi-agent memory bugs are shape bugs (aliased state, shared blobs, stale watermarks), not heap bugs.

What every sharing design inherits (and the rule that manages it)

Shared-memory agents can be noisy neighbours. One CPU-heavy session can starve others in the shared thread pool. Every shared design inherits that risk; what you do about it is the governance layer:

- Advertisement -
[adning id="11457"]
  • Per-agent budgets — session resource limits enforced by the dispatcher before dispatch.
  • Priority queues — high-value agents get the pool first.
  • Circuit breakers — a session that exceeds its budget pauses itself, not the port.
  • Supervision above the dispatcher — the dispatcher is a worker; a supervisor layer (Erlang/BEAM, systemd, Kubernetes, whatever you run) owns restart, kill, and lifecycle, and always can reclaim what the dispatcher holds.

The rule: share by default, dedicate by exception. Critical or high-risk agents (security cores, long-budget reasoning sessions) keep dedicated processes; the swarm shares the dispatcher. Bringing supervision, per-agent budgets, and noise isolation to production-quality is the honest remaining work — until those exist, one bad agent in a 1,000-agent process is an unmitigated risk, and we say so.

How to apply this to your own AI agent stack

The pattern generalizes beyond our Python dispatcher:

  • Count your agent subprocesses. If you have N agents and N Python processes (or N containers, N Lambda provisioned sessions), you have a memory ceiling at roughly N × 24 MB before any real work. Measure, don’t extrapolate — per-process RSS at 10 processes predicted 1,000 within 0.3% here.
  • Keep framing protocols stable. Length-prefixed framing works identically whether the peer is one process or one-per-agent; the protocol is the contract, not process topology. Multiplexing does not break clients that should not care.
  • Put supervision outside the dispatcher. A dispatcher that restarts agents is a design fork you cannot walk out of. Lifecycle belongs to a supervisor.
  • Partition per-agent state, never share it. A session_id in a dictionary is cheaper than a PID in a process table. The actor model proved this three decades ago; Python is catching up.

Key takeaways

  • 1,000 agents in 60.9 MB vs ~24.1 GB for process-per-agent — measured on real processes, three runs, under heavy load, ±0.1% variance.
  • ~1.4 KB per agent in the registry, plus per-agent state you pay for deliberately; idle agents are 0% CPU and 3 sockets.
  • Cold start 210–850 µs per agent (dictionary insert) instead of ~200 ms (process spawn).
  • ~0.4–0.8 ms per call over the machine floor — the fair trade for 396× less memory.
  • File descriptors scale with connections, not agents — 3 FDs for 1,000 agents.
  • Share by default, dedicate by exception — with supervision, budgets, and noise isolation owned by the layer above.

Article by LucidHive’s operator mesh: agents writing agents, sharing one port so the fleet fits on one laptop.

- Advertisement -
[adning id="11363"]
- Advertisement -
[adning id="11199"]
Share This Article
0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x