🐛 Bug Report: Hardcoded memory_limit 512M crashes Realtime and drops all clients at ~3,000 connections per container · Issue #14437 · appwrite/appwrite · GitHub
Skip to content

🐛 Bug Report: Hardcoded memory_limit 512M crashes Realtime and drops all clients at ~3,000 connections per container #14437

Description

@JoshiJoshiJoshi

👟 Reproduction steps

  1. Run Appwrite 2.4.0 self-hosted with the default Realtime setup: one worker (_APP_WORKERS_NUM unset, see realtime.php L367–L373) and the default pool size.
  2. Create a table with read("any").
  3. Open a few thousand WebSocket connections at once (a reconnect storm, as after a deploy or a network blip) to /v1/realtime?project=…&channels[]=tablesdb.<db>.tables.<table>.rows, e.g. with k6 k6/websockets.
  4. Write one row every 2 s through the REST API and count the events per client.
  5. Watch docker logs appwrite-realtime.

👍 Expected behavior

Realtime keeps the connections open and delivers events, or degrades gracefully (slower connects, higher latency, or rejecting new connections with a clear close code). A worker should not crash because it holds many connections.

👎 Actual Behavior

app/init.php sets \ini_set('memory_limit', '512M'); for every Appwrite PHP process (init.php#L17 on 2.4.0). It overrides php -d memory_limit=… and any ini file, so operators cannot raise it. A Realtime worker keeps all its connections and subscriptions in memory, and when it reaches 512 MB it dies with all of them.

Appwrite 2.4.0, default single worker (two Realtime nodes, all clients connect at once and hold for 30 s, one write every 2 s, nofile 65536):

Workers / pool Clients (total, 2 nodes) Result Connect p50 / p99 Events delivered
1 / 16 4,000 (2,000 per node) 3,750 and 4,000 connected¹, no drops, 2 runs 2.7–3.7 s / 3.4–5.2 s 100 %
1 / 16 6,000 (3,000 per node) crash on both nodes, all clients dropped, 3 of 3 runs – 0 %
1 / 1 2,000 (1,000 per node) 1,750 connected¹, slow, 3 runs 16.3–18.7 s / 25.1–25.5 s 32–34 %

¹ One load-generator VU (250 clients) occasionally gets neither an error nor a response; this also happened on 2.3.0 in unrelated configurations and is an artefact of the generator.

The crash, on both nodes, every time:

Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 20480 bytes) in /usr/src/code/packages/cache/src/Codec/Igbinary.php on line 41
ERROR php_swoole_server_rshutdown() (ERRNO 503): Fatal error: Allowed memory size of 536870912 bytes exhausted ...

The container restarts (RestartCount +1). With one worker per container, that means every client of the node is disconnected at once, and their reconnects form the next storm.

The comment in realtime.php explains the single-worker default: a worker with ~1,200 connections needs little CPU, and concurrency is the deployment's job. That's fair for CPU, but the memory limit caps each container at roughly 2,500–3,000 connections during a reconnect storm, independent of the host's RAM. Scaling out then needs more Realtime containers behind a load balancer, which the default setup doesn't provide.

With a pool of 1 (step 3 of the table), handshakes queue for the single database connection: connecting 1,000 clients per node takes 16–19 s and only a third of the events reach the clients in the window. On 2.3.0 the same setup crashed at 5,000 clients (below).

Appwrite 2.3.0, more workers and a raised limit (same cluster, 45 s hold; 2G = init.php patched to read the limit from an environment variable):

Workers / pool / memory_limit Clients (total, 2 nodes) Connected Dropped after connect Connect p50 Event latency p50
1 / 1 / 512M (default) 2 000 2 000 0 18.3 s 2.7 s
1 / 1 / 512M (default) 5 000 0 all, worker crashed, container restarted – –
2 / 16 / 512M 10 000 10 000 0 3.1 s 0.25 s
2 / 16 / 512M 20 000 ≈ 3 000 ≈ 19 000 (worker crash) – –
4 / 16 / 512M 20 000 20 000 0 3.2 s 0.34 s
4 / 16 / 512M 30 000 29 937 4 160 (worker crash) 10.5 s 0.46 s
2 / 16 / 2G (patched) 20 000 20 000 0 5.7 s 0.50 s
2 / 16 / 2G (patched) 40 000 40 000 0 17.3 s 1.2 s
4 / 16 / 2G (patched) 30 000 30 000 0 6.6 s 0.61 s
  • With the default single worker and pool 1, Realtime uses only 13–30 % CPU during the storm. It is waiting, not working.
  • Pool 16 alone makes the connect time of 2 000 clients go from 14.6 s to 1.3 s and the delivery within 40 s from 50 % to 100 % (separate run).
  • With the memory limit raised (patched init.php), no worker crashed up to 40 000 clients. A worker then used up to ≈ 1.8 GB RSS. Connect time and latency grow with the client count, but the connections are held.

The same limit elsewhere:

🎲 Appwrite version

2.4.0 (measured), also 2.3.0. The hardcoded limit has been there since at least 1.3 (#5528).

💻 Operating system

Linux (Debian 12), Docker

🧱 Your Environment

Self-hosted, Postgres adapter. Realtime on two nodes with 4 vCPU and 12 GB each, ulimits.nofile 65536 (with the image default of 1,024, Realtime stops accepting at ≈ 1,000 connections per node, without an error to the client). Postgres and Redis on a third node.

💡 Suggested fix

  1. Make memory_limit configurable, e.g. _APP_MEMORY_LIMIT (default 512M), or don't override it in init.php and let the image's php.ini decide. Make the hardcoded PHP memory_limit configurable #13162 does this and has been open since August.
  2. Size the Realtime pool for handshake storms: pass _APP_WORKER_MAX_COROUTINES (or a dedicated variable) to appwrite-realtime in the default docker-compose.yml.
  3. Fail gracefully near the limit: reject new connections with a close code instead of crashing the worker and dropping every existing connection. fix(realtime): bound the per-connection output buffer #13415 (bounding the per-connection output buffer) helps with one source of memory growth.
  4. Document the per-container connection limit and ulimits.nofile for Realtime.

Risks / things to consider for the fix

  • A higher limit means more RAM per worker. Operators need to size the host (we measured ≈ 2.3 GB RSS per 10 000 clients on one node).
  • More workers spread connections and memory, but on a shared API node they also take CPU from the API. With 8 workers on 4 vCPU, a storm of 40 000 clients took all CPU of the node and slowed the API to 4 s per request.

👀 Have you spent some time to check if this issue has been raised before?

🏢 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 / databasesFixes and upgrades for the Appwrite Database.
    product / functionsFixes and upgrades for the Appwrite Functions.
    product / self-hostedIssues only found when self-hosting Appwrite
    product / sitesFixes and upgrades for Appwrite Sites.
    product / storageFixes and upgrades for the Appwrite Storage.
    sdk / cliFixes and upgrades for the Appwrite CLI.
    on Oct 13, 2026
  2. ItsRoy69 commented on Oct 13, 2026

    @ItsRoy69
    Contributor

    Can work on this issue

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 / databasesFixes and upgrades for the Appwrite Database.product / functionsFixes and upgrades for the Appwrite Functions.product / self-hostedIssues only found when self-hosting Appwriteproduct / sitesFixes and upgrades for Appwrite Sites.product / storageFixes and upgrades for the Appwrite Storage.sdk / 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