limits #
One replay spends other people's resources: up to ~150 MB of archive reads
from a shared box, a socket on a public Jetstream instance, sixteen HTTP
connections in flight. The endpoint is open, so every caller is a stranger.
Three layers bound what a stranger can spend; all live in guard.py.
per-caller rate limit #
Tool calls are metered per client address (the first hop of
x-forwarded-for behind Horizon's proxy) with a token bucket: a burst of 4,
then one call every 2 seconds. initialize and tools/list are not metered,
so a client can always connect and see the tools. Over the limit, the call
fails with a rate limit error naming the numbers.
Callers behind one NAT share a bucket; a caller with many addresses does not. That is what the global caps are for.
global slots #
Two semaphores cap concurrent expensive work across all sessions:
| work | slots | wait |
|---|---|---|
| archive reads | 4 | 10s |
| socket tails | 8 | 10s |
A call that cannot get a slot within the wait fails with a busy error
rather than queueing indefinitely. The numbers are first guesses sized to
the archive key's 100 Mbps meter (four full replays at once is roughly the
meter) and should be retuned from the box's rate_bps metrics, not from
this table.
caches #
| what | keyed by | lifetime |
|---|---|---|
| segment list | archive host | forever, topped up incrementally |
| resolved session | the stream argument |
forever if ended, 30s if live |
| archive window | streamer, window, sealed edge | forever if the window ended before the sealed edge, else 60s |
| handle and account status | DID | 1h |
A sealed window reads the same forever, so a popular historical stream costs the archive one read and every later caller gets it from memory. Caches are per process and bounded by entry count; a redeploy starts cold.
upstream backstops #
The archive key on stream.waow.tech is metered at 100 Mbps; past that the box answers 429 and block fetches back off 1s/2s/4s before failing. The public Jetstream instances are shared infrastructure with their own admission limits; the tail opens one socket per call and closes it when the budget ends.
what is not bounded #
Memory for cached windows (64 entries, each a session's worth of rows) and
the size of a single response (limit defaults to 200 messages, maximum
1000). Neither has been measured under load; if the process grows, the
window cache is the first place to look.
watching it #
/stats on the hosted server shows this process's counters: calls by tool
with p50/p95, why replays stopped, refusals by the limiter and the slots,
archive bytes and cache hits, socket time. They reset on deploy. The durable
record is Horizon's usage API; scripts/usage.py prints calls, error rate,
latency, and callers by client name for a date range.