🐛 Bug Report: Redis connections have no read timeout: an unresponsive Redis hangs every request, and after ≈ 4 s the node answers 503 to everything with an empty body and no log entry · Issue #14490 · appwrite/appwrite · GitHub
Skip to content

🐛 Bug Report: Redis connections have no read timeout: an unresponsive Redis hangs every request, and after ≈ 4 s the node answers 503 to everything with an empty body and no log entry #14490

Open

Description

@JoshiJoshiJoshi

👟 Reproduction steps

  1. Self-hosted Appwrite with the official docker-compose.yml (we measured 2.4.0, the code is unchanged in 2.5.0), API on two nodes, Redis on a third.
  2. Put steady load on the API: 400 req/s GET of a row plus 100 req/s of a session-authenticated read, split over both API nodes, 180 s, client timeout 15 s.
  3. After 30 s make Redis stop answering for 60 s, without closing the connection: docker pause appwrite-redis (this is what a network blackhole, a frozen VM or a very long blocking command looks like to the API). Then docker unpause appwrite-redis.
  4. For comparison, the same with docker stop appwrite-redis / docker start appwrite-redis.
  5. While Redis is paused, call GET /v1/health/version on an API node.

👍 Expected behavior

A Redis that does not answer produces a quick error after a bounded time (1–2 s), written to the log, and requests that do not need Redis keep working. The node recovers as soon as Redis does.

👎 Actual Behavior

With docker pause, requests hang for as long as Redis is silent. After ≈ 4 s each API node has 1 000 requests in flight, and from then on Swoole rejects every new request immediately with 503 and an empty body, including /v1/health/version. Nothing is written to the API log for these rejections. This lasts for the whole 60 s; the node recovers ≈ 2 s after the unpause.

Two runs per variant, outage window s 30–94, g_row (400 req/s, both nodes together):

Run Before (s 0–28) During the outage After (s 95–110) Disturbed until
pause, run 1 11 599 × 200 1 316 × 200, 23 086 × 503 (answered in 1–2 ms), 1 598 without answer (client timeout) 6 400 × 200 s 92 (≈ immediately)
pause, run 2 11 600 × 200 1 349 × 200, 23 052 × 503, 1 599 without answer 6 400 × 200 s 92
stop, run 1 11 600 × 200, p50 14 ms 15 900 × 200 with p50 4.1 s, 10 093 × 503 3 662 × 200, 2 738 × 503 s 102 (+7 s)
stop, run 2 11 600 × 200 15 575 × 200 with p50 4.0 s, 10 420 × 503 5 647 × 200, 753 × 503 s 97 (+2 s)

The session-authenticated read behaves the same way. With stop, Redis refuses the connection, every request retries ("Redis at redis:6379 after 5 attempts: Connection refused", 42 020 log lines in 6 minutes) and still about 60 % succeed, but slowly. After the start Redis loads its RDB file and answers LOADING for 2–7 s, which is also 503.

Cause

Same 503 path without Redis: one client with many connections

The 503-without-log behaviour is also reached by load alone. In a 60-minute soak (≈ 390 req/s of normal traffic on two nodes) we opened 1 500 permanently open connections from one client against one node, twice for 60 s. That node answered ≈ 90 % of all requests with 503 (minute 20: 32 054 × 200 / 294 344 × 503; minute 40: 30 747 / 277 041), at ≈ 5 300 responses/s. In the same time ≈ 85 % of the normal requests of all other clients on that node were rejected too. This is the same max_concurrency limit. It is intended protection, but the rejection is silent and not attributable to a client; together with the missing read timeout it turns any slow dependency into a full outage of the node.

🎲 Appwrite version

2.5.0 (measured on 2.4.0, code unchanged in 2.5.0)

💻 Operating system

Linux (Debian 12), Docker

🧱 Your Environment

Self-hosted Appwrite 2.4.0, Postgres adapter, Redis 7.4 from the official compose file, two API nodes (Swoole) and one data node, docker pause/unpause and stop/start for the outage.

💡 Suggested fix

  • Set a read timeout and a connect timeout on the Redis connections (for example 1–2 s) instead of OPT_READ_TIMEOUT = -1, so a silent Redis becomes a fast error. Long blocking calls (BRPOP, pub/sub subscribe) would need their own larger timeout, or the same value plus their own wait time.
  • Log the resulting Redis error with the request path, so that the outage is visible.
  • Log (rate limited) and count when max_concurrency rejects requests, for example one line per second with the current in-flight number, and expose the counter in the health endpoints or telemetry. Today these 503s leave no trace.
  • Optionally let operators set max_concurrency via an environment variable, and document that a load balancer should limit connections per client.

👀 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 / storageFixes and upgrades for the Appwrite Storage.
    sdk / cliFixes and upgrades for the Appwrite CLI.
    on Oct 14, 2026
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 / databasesFixes and upgrades for the Appwrite Database.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