Skip to content

sec: add per-conn publish rate limit alongside per-key bucket - #21

Merged
EthanY33 merged 1 commit into
mainfrom
sec/per-conn-rate-limit
May 7, 2026
Merged

sec: add per-conn publish rate limit alongside per-key bucket#21
EthanY33 merged 1 commit into
mainfrom
sec/per-conn-rate-limit

Conversation

@EthanY33

@EthanY33 EthanY33 commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #11.

The per-API-key bucket (c.rateLimit.Allow(c.apiKeyID)) is shared across every connection a key owns. That's correct for cross-conn fairness, but it doesn't bound what a single socket can push at the moment — a key with N conns drains the bucket N× faster, but each individual socket is otherwise capped only by the 64KB read limit and network RTT.

Adds a per-conn rate.Limiter from golang.org/x/time/rate (already a transitive dep of the Prometheus client). Default 50/s sustained, burst 100 per conn — generous for legitimate UI clients and well below what's needed to amplify a 64KB message into a broadcast DoS at defaultMaxSubsPerChannel = 10000.

handlePublish now checks per-key first, then per-conn. RATE_LIMITED is unchanged for the per-key path; new RATE_LIMITED_CONN distinguishes the per-conn case so a client knows whether to slow down or open another connection.

What changed

  • internal/conn/conn.go:
    • Conn.connRate *rate.Limiter field.
    • defaultConnPublishRate = 50, defaultConnPublishBurst = 100.
    • Initialized in Run.
  • internal/conn/handler.go: handlePublish runs the per-conn check after the per-key check.

Test plan

  • go build ./... clean.
  • go test ./internal/conn/... passes — existing tests publish a handful of messages, well within the 100-burst budget.
  • Stress test: single conn pushing >100 publishes/sec receives RATE_LIMITED_CONN, while a second conn (same key) keeps publishing at its own limit until the per-key bucket drains.

Conflict notes

Touches internal/conn/conn.go and internal/conn/handler.go. Conflicts at merge time with PR #20 (channel GC + sub rate-limit), which also adds rate-limit calls to subscribe/unsubscribe. Both are additive; resolution is keeping both calls in each handler.

Out of scope

  • Lowering defaultMaxSubsPerChannel (the audit suggested 10000 is too high). Behavior change worth a separate decision; flagged as TODO in conn.go.
  • Byte-rate limiting (in addition to message-count). Most legitimate apps publish small messages; this can land if real-world traffic shows it's needed.

Closes #11.

The existing per-API-key bucket (rateLimit) is shared across every
connection a key owns. That's correct for fairness across the key's
clients, but it doesn't bound what a single socket can push at the
moment — a key with N conns sees the bucket drain N× faster but each
individual socket is still capped only by the 64KB read limit and the
network round-trip.

Adds a per-conn rate.Limiter (golang.org/x/time/rate, already a dep
via prometheus client). Default 50 publishes/sec sustained, burst
100 — generous for legitimate UI clients (chat apps see far less)
and well below what's needed to amplify a 64KB message into a
broadcast DoS at the per-channel cap of 10000 subscribers.

handlePublish now checks per-key first (existing behavior), then
per-conn. RATE_LIMITED stays for per-key over-budget; new
RATE_LIMITED_CONN distinguishes the per-conn case so a chatty client
can attribute the slowdown.

Tunable via defaultConnPublishRate / defaultConnPublishBurst
constants in conn.go; flag wiring deferred to whichever PR ends up
landing the broader --publish-rate / --publish-burst flags.
@EthanY33
EthanY33 merged commit 51b9960 into main May 7, 2026
2 checks passed
@EthanY33
EthanY33 deleted the sec/per-conn-rate-limit branch May 7, 2026 23:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Medium] BroadcastMu causes head-of-line block on slow consumers

1 participant