🐛 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
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: 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
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.
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.
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.
For comparison, the same with docker stop appwrite-redis / docker start appwrite-redis.
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.
The server caps in-flight requests at max_concurrency = 1_000 (packages/http/src/Adapter/Swoole/Mode.php L106). Requests that wait for Redis never finish, so the cap is reached within seconds, and Swoole answers everything above it with 503 before Appwrite code runs. That is why even /v1/health/version fails and why there is no log line.
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?
Activity