Status: implemented, pre-alpha defaults
The daemon combines hard size, queue, connection, page, timeout, and worker
bounds with token-bucket throughput limits. A token bucket permits its configured
burst immediately, then restores per_minute tokens continuously. The
implementation uses integer time accounting rather than floating point, so the
same configuration has deterministic capacity and no cumulative rounding drift.
The checked-in profiles currently use these policies:
| Policy | Scope | Per minute | Burst | Covered work |
|---|---|---|---|---|
| Protocol | Each IRC or native WebSocket connection | 600 | 60 | Every parsed command except IRC PONG, QUIT, and ERROR |
| Admission | Whole process, across all listeners | 120 | 64 | Accepted TCP connections before TLS, WebSocket, or HTTP work |
| Authentication | Whole process | 120 | 64 | SASL and native-control password verifications |
| Invitation | Ephemeral native-control source bucket | 5 | 5 | Redemption and, separately, authenticated issue/revoke mutations |
| Messages | Each IRC or native WebSocket connection | 120 | 20 | PRIVMSG, NOTICE, TAGMSG, native message.send, and automation.invoke |
| History | Each IRC or native WebSocket connection | 60 | 20 | CHATHISTORY, MARKREAD, MARKDELIVERED, native sync.resume, and native delivery.ack |
| Administration | Each IRC or native WebSocket connection | 60 | 20 | IRC state commands and native channel.join |
These are independent budgets. Exhausting history does not prevent a user from
chatting or sending PONG, and a message flood cannot consume the authority
mutation budget. One multi-target command consumes one token, while existing
target-count and output-queue bounds limit its fanout.
Admission remains additionally bounded by limits.max_connections and the
registration timeout. Argon2 password verification and channel-key work remain
separately concurrency-bounded at four workers. Invitation hashing, SQLite actor
queues, outbound client queues, history pages, and command target lists retain
their existing hard capacities. Rate limits supplement those bounds; they do
not replace them.
- Admission rejection closes the newly accepted socket before protocol work.
- An IRC command rejected by a per-connection budget receives numeric
263(RPL_TRYAGAIN). RejectedNOTICEandTAGMSGcommands remain silent.PONG,QUIT, andERRORalways bypass the protocol bucket. - A server-wide SASL rejection is deliberately indistinguishable from another
authentication failure (
904). It does not disclose whether an account, device, or credential exists. - A native sync command receives a requester-correlated
rate_limitederror withretryable: true; exhausting one typed bucket does not consume another. - Native-control source or global authentication rejection returns HTTP 429
with
Retry-After: 60. Invitation redemption otherwise retains its genericregistration_unavailableresponse.
The HTTP onboarding edge also retains a fixed 20-token, 20-per-minute authentication-attempt bucket per ephemeral source key before it performs Basic authentication. This intentionally tighter edge policy is not a TOML setting.
After successful authentication, automated-account provisioning,
owner/operator automation-policy mutations, and configured-operator capability
changes use distinct ephemeral source buckets with the configured
limits.administration rate and burst. Account-avatar owner set/remove and
account nickname-color replacement use a dedicated identity-mutation source
bucket; operator avatar clear shares the configured-operator mutation bucket.
Image decoding is limited to two workers.
Authenticated avatar reads use the ordinary control authentication bucket and
bounded password-verification pool. Exhausting one mutation class does not
consume the others.
IRC, WebSocket, and global admission/authentication limiters do not retain or group client addresses. This avoids turning a transport address into a durable identity and behaves predictably behind NAT. The tradeoff is explicit: one source can temporarily consume a process-wide admission or authentication budget and delay unrelated users. For a small private node, a hard anonymous resource ceiling is preferable to retaining a server-side address history. A future deployment should change that only in response to observed abuse and a reviewed proxy/address trust model.
The native-control edge needs source isolation because it accepts bearer-token
and password attempts before a session exists. It hashes each source address
with a process-random key, stores at most 2,048 (hash, class) entries, expires
idle entries, and rotates the key and clears the map every minute. Raw addresses
and stable derivatives are never logged or persisted. A reverse proxy is seen
as one source unless it uses reviewed layer-4 pass-through; untrusted forwarding
headers are ignored.
External-content capability redemption has its own source-attempt class at that edge, separate from invitation and ordinary control authentication. Catalogue and capability issuance use durable one-hour counters keyed by requester and channel so reconnecting or switching devices cannot reset them. The fixed v1 allowances are 60 searches, 60 requests, 64 GiB of claimed bytes, and four outstanding capabilities for a human; 12, 12, 8 GiB, and two for ordinary automation; and 1,000, 1,000, 1 TiB, and 16 for specifically authorized bulk automation. Exact idempotent retries are not charged twice. These are policy guardrails rather than exported diagnostics dimensions; see external content references.
Operational counters contain only aggregate accepted/refused totals, the seven
policy names above, and a heartbeat-timeout total. The heartbeat counter
increments only when a registered connection fails to return the server's
matching PONG; peer closes and ordinary reconnects do not increment it. None
of these counters has an address, account, device, nickname, channel, target,
message, DM relationship, or private-membership label.
Rejections log only the first and power-of-two aggregate totals to resist log
amplification, and a complete aggregate snapshot is logged during clean
shutdown. Counters are process-local and reset on restart.
GET /v1/diagnostics exposes the same snapshot only to an account with
diagnostics.read; it does not add identifying dimensions or a live-presence
count.
Automation guardrail accounting is a separate authorization mechanism, not a diagnostics metric. It necessarily keys one-minute counters by the durable bot account and channel whose shared allowance is being enforced, but never exports those keys through diagnostics. Repeated-content detection stores only a channel-scoped SHA-256 digest of lowercased, whitespace-collapsed text. Idle counters and digests are deleted after the window by normal maintenance.
Offline administrative commands are deliberately outside these runtime token buckets. They require local operating-system authority, acquire the exclusive database lease while the service is stopped, execute one bounded operation, and exit; an operator should use ordinary host process controls for that trust boundary.
Each nested table accepts per_minute and burst. Both must be non-zero;
per_minute is capped at 60,000 and burst at 10,000. Omitting either value
retains that field's compiled default.
[limits.protocol]
per_minute = 600
burst = 60
[limits.admission]
per_minute = 120
burst = 64
[limits.authentication]
per_minute = 120
burst = 64
[limits.invitation]
per_minute = 5
burst = 5
[limits.messages]
per_minute = 120
burst = 20
[limits.history]
per_minute = 60
burst = 20
[limits.administration]
per_minute = 60
burst = 20Tune from measurements, not merely to suppress a rejection. Large bursts move work back into the hard queues; very small bursts can break reconnects that legitimately restore several channels and cursors at once. Native automation uses these typed protocol, history, message, and administration classes and an additional durable channel policy. The latter defaults to 60 messages/32 KiB per account and 240 messages/128 KiB across the channel in a fixed one-minute window. It also enforces repeated-content and causal-loop tripwires, a 30-second account/channel cooldown, and a revisioned moderator pause. Exact idempotent retries do not spend the allowance twice. See automated participants for tuning bounds and suspension behavior.