🐛 Bug Report: Unbounded realtime subscriptions per connection crash the realtime worker and disconnect all its clients · Issue #14430 · appwrite/appwrite · GitHub
Skip to content

🐛 Bug Report: Unbounded realtime subscriptions per connection crash the realtime worker and disconnect all its clients #14430

Description

@JoshiJoshiJoshi

👟 Reproduction steps

  1. 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).
  2. Keep going to ~169,500 subscriptions on the one connection.
  3. 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?

  • I checked and didn't find a similar issue

🏢 Have you read the Code of Conduct?

  • I have read the Code of Conduct

Activity

  1. added
    api / realtimeFixes and upgrades for the Appwrite Realtime API.
    product / authFixes and upgrades for the Appwrite Auth / Users / Teams services.
    product / databasesFixes and upgrades for the Appwrite Database.
    product / self-hostedIssues only found when self-hosting Appwrite
    sdk / cliFixes and upgrades for the Appwrite CLI.
    on Oct 13, 2026
  2. jackeddisciple commented on Oct 13, 2026

    @jackeddisciple

    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?

  3. added a commit that references this issue on Oct 15, 2026
    6c98fe6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api / realtimeFixes and upgrades for the Appwrite Realtime API.product / authFixes and upgrades for the Appwrite Auth / Users / Teams services.product / consoleConsole, UI and UX issuesproduct / databasesFixes and upgrades for the Appwrite Database.product / self-hostedIssues only found when self-hosting Appwritesdk / cliFixes and upgrades for the Appwrite CLI.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions