🐛 Bug Report: Unbounded realtime subscriptions per connection crash the realtime worker and disconnect all its clients · Issue #14430 · appwrite/appwrite · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
🐛 Bug Report: Unbounded realtime subscriptions per connection crash the realtime worker and disconnect all its clients #14430
Open a realtime connection and subscribe to a public channel (e.g. a table channel with read("any")), repeatedly, each subscribe message carrying ~300 subscriptions with distinct subscriptionIds and a small filter (one subscribe message ≈ 45 KB; the server rejects messages over 64 000 bytes, so 300 is near the max per message).
Keep going to ~169,500 subscriptions on the one connection.
A second, normal client is connected to the same worker.
Observed, 2 of 2 runs:
Subscriptions on the one connection
Realtime worker RSS
Victim event latency
0
~75 MB
50 ms
50,000
~350 MB
150 ms
150,000
~480 MB
752 ms
~169,500
—
worker crashes, victim disconnected
Worker log: Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 10485760 bytes) in src/Appwrite/Messaging/Adapter/Realtime.php on line 239. The container's RestartCount increases by one each time; the victim connection closes (eof) and receives no further events.
👍 Expected behavior
The number of subscriptions per connection is limited, and one connection cannot take down the realtime worker or affect other clients.
👎 Actual Behavior
A single realtime WebSocket connection can register an unbounded number of subscriptions. There is no per-connection (or per-user) subscription limit. Enough subscriptions exhaust the realtime worker's PHP memory limit (512 MB); the worker process dies with a fatal error and every other connection served by that worker is dropped. On a default self-hosted install the realtime service runs few worker processes (one per core, _APP_WORKERS_NUM), so one abusive connection can repeatedly disconnect all realtime clients on a node.
Cause
No limit on the number of subscriptions per connection (Realtime::subscribe() / the subscribe handler never bound the count).
getSubscriptionMetadata() (Realtime.php L213–L251) rebuilds the full metadata array for the connection, and it is called on every subscribe/unsubscribe (\count($realtime->getSubscriptionMetadata($connectionId)) in both handlers) — O(total subscriptions) allocation per message, so memory grows super-linearly with traffic and the big array is what trips the 512 MB limit (L239).
The abuse limit is 32 messages/min/connection and only when _APP_OPTIONS_ABUSE=enabled; it is per connection, so an attacker spreads the load over several connections (each adds to the same worker's memory), and on installs with abuse disabled there is no limit at all.
Also a performance effect short of the crash: getSubscribers() iterates every subscription on a channel/role for each event, so many subscriptions on a shared channel raise delivery latency for all clients on that channel (50 ms → 752 ms at 150k here).
Impact
Any client that may open a realtime connection and subscribe to a channel can crash the realtime worker and disconnect all other realtime clients on that node, repeatedly. Denial of service for realtime (live queries, presence, console live events).
🎲 Appwrite version
2.4.0 (measured). setPackageMaxLength(64000) in app/realtime.php L377; the abuse limit (32 messages/min/connection) applies only with _APP_OPTIONS_ABUSE=enabled (L1328–L1329).
💻 Operating system
Linux (Debian 12), Docker
🧱 Your Environment
Self-hosted, two API nodes, realtime with the default worker count and the hard-coded 512 MB PHP memory limit.
💡 Suggested fix
Cap subscriptions per connection (and/or per user/project), rejecting further subscribe with a clear error instead of letting memory grow unbounded.
Avoid rebuilding the whole metadata array on every subscribe/unsubscribe: track the count incrementally, or compute the delta without materialising getSubscriptionMetadata().
Consider counting subscriptions (not just messages) against the abuse limit.
We ran it on a public table channel (read("any")). We did not test separately whether a connection without a session (guest) can do the same.
Proposed severity: Medium–High. CWE-770 (Allocation of Resources Without Limits or Throttling). CVSS 3.1 AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H (6.5) assuming a logged-in user; PR:N (7.5) if guests on public channels can do the same.
👀 Have you spent some time to check if this issue has been raised before?
Activity
jackeddisciple commentedon Oct 13, 2026
Hey, I'd like to work on this issue. Is someone already working on it, or would it be okay for me to take it up?
fix(realtime): limit subscriptions per connection