Diagnosing Appwrite Realtime WebSocket Drops and Failed Reconnections
A diagnostic guide for Appwrite Realtime WebSocket connection drops: recognize symptoms, isolate root causes (proxy timeouts, worker crashes, SDK mismatch, JWT expiry, browser throttling), run ordered checks, apply targeted fixes, and know when to escalate.
08 Aug 2026, 13:09 UTC

The Problem: Silent WebSocket Death
Your Appwrite client shows Realtime disconnected repeatedly. The UI goes stale—document creates, updates, and deletes stop appearing. After a network hiccup or server restart, the client sits in a perpetual connecting state. This guide walks through recognizing the condition, isolating the root cause, and applying targeted fixes for Appwrite v1.4.x–v1.5.x.
Recognizable Condition
- Browser console: repeated
WebSocket connection closedorRealtime disconnectedevents - Failed reconnection attempts (exponential backoff stalls or gives up)
- UI symptoms: stale data, missed realtime events, perpetual "connecting" indicator
- Triggers: network blips, server restarts, tab backgrounding, or JWT expiry
Cause & Diagnostic Table
| # | Root Cause | Observable Evidence | Typical Close Code |
|---|---|---|---|
| 1 | Proxy/load balancer idle timeout < Appwrite ping interval (default 30s) | Silent TCP drop; no close frame in DevTools | 1006 (abnormal closure) |
| 2 | Realtime worker crash or OOM restart | Server logs show worker restart; no graceful close sent | 1001 (going away) or 1006 |
| 3 | Client SDK version mismatch (pre-1.4 SDK with v1.5 server) | Console: Realtime protocol version mismatch |
4000+ (protocol error) |
| 4 | JWT expired during long-lived connection | Re-auth handshake rejected on reconnect | 4001 (unauthorized) or 4003 (forbidden) |
| 5 | Browser tab throttling/background suspension | Missed pongs after tab backgrounded >5 min (Chrome) or suspended (Safari) | 1006 or 1000 (normal) after server timeout |
Ordered Verification Steps
1. Capture WebSocket Frames in DevTools
Where: Browser DevTools → Network tab → WS filter → select the /v1/realtime connection → Frames panel.
Check: Ping/pong frames every ~30 seconds. Note the close code when connection drops:
1000= normal closure1001= endpoint going away (server restart)1006= abnormal closure (network/proxy drop)4000+= Appwrite protocol error (auth, version, permission)
Risk: None—read-only inspection.
2. Inspect Appwrite Realtime Service Logs
Where: On the Appwrite server host (or via Docker):
docker logs appwrite-realtime -f --tail 200
Look for: Lines containing connection closed, worker restarts, OOM, or health check failed. Correlate timestamps with client disconnects.
Permissions: Docker access or server SSH.
3. Verify Load Balancer/Proxy Configuration
Check: Idle timeout ≥ 45 seconds (Appwrite pings every 30s; add margin). WebSocket upgrade headers must pass through.
Common defaults that break WebSockets:
- AWS ALB: 60s idle timeout (configurable to 4000s via
idle_timeout.timeout_seconds) - Cloudflare: Enable "WebSocket" toggle in Network tab; proxy mode must be "Proxied"
- NGINX:
proxy_read_timeout 300s;andproxy_send_timeout 300s;in location block
Verification: wscat -c wss://<your-host>/v1/realtime -H "Authorization: Bearer <JWT>" — confirm handshake completes and pings arrive.
4. Validate SDK/Server Version Alignment
Check: npm list @appwrite/sdk-for-web (or your platform SDK). Major version must match server major version (v1.4.x server → SDK ^1.4.0; v1.5.x server → SDK ^1.5.0).
Evidence: Console error Realtime protocol version mismatch confirms mismatch.
Fix: npm install @appwrite/sdk-for-web@^1.5.0 (pin to server major).
5. Test Minimal Subscription Scope
Where: Client code — temporarily subscribe to a single collection with known permissions:
const client = new Client().setEndpoint('https://<host>/v1').setProject('<project-id>');
const realtime = new Realtime(client);
realtime.subscribe(['databases.<db-id>.collections.<col-id>.documents'], (response) => {
console.log('Event:', response);
});
Purpose: Rules out permission/collection-scope issues. If minimal sub works, the problem is subscription breadth or permission logic.
Targeted Fixes Mapped to Findings
Fix 1: Proxy Idle Timeout & WebSocket Passthrough
Set idle timeout to 60s+ on all hops. Example NGINX snippet:
location /v1/realtime {
proxy_pass http://appwrite-realtime:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
Verification: Re-run wscat test; confirm no silent drops after 60s idle.
Fix 2: Realtime Worker Memory & Scaling
Monitor worker memory via docker stats appwrite-realtime. If OOM kills appear:
- Increase container memory limit:
docker update --memory=2g appwrite-realtime - Or scale replicas (docker-compose):
deploy: replicas: 3 resources: limits: memory: 1G
Ensure health-check endpoint (/v1/health) responds <200ms under load.
Fix 3: SDK Version Pinning
Add to package.json:
"@appwrite/sdk-for-web": "^1.5.0"
Run npm ci in CI/CD to enforce. Check SDK changelog for Realtime protocol version bumps matching your server.
Fix 4: Proactive JWT Refresh Before Expiry
Default JWT TTL = 1 hour. Long-lived connections will hit expiry. Implement a refresh listener:
// Refresh 5 minutes before expiry
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
async function scheduleTokenRefresh() {
const session = await account.getSession('current');
const expiry = new Date(session.$createdAt).getTime() + (session.expire * 1000);
const delay = Math.max(0, expiry - Date.now() - REFRESH_BUFFER_MS);
setTimeout(async () => {
const newJwt = await account.createJWT();
realtime.connect({ token: newJwt.jwt }); // Re-establish with fresh token
scheduleTokenRefresh(); // Schedule next
}, delay);
}
realtime.on('close', () => scheduleTokenRefresh());
Risk: Clock skew between client/server; use server time if available.
Fix 5: Background Tab Reconnection with Page Visibility API
Browsers throttle timers in background tabs (Chrome: 1/min after 5min; Safari: suspends WS). Force reconnect on focus:
let reconnectAttempt = 0;
const MAX_BACKOFF_MS = 30000;
function connectWithBackoff() {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempt), MAX_BACKOFF_MS);
setTimeout(() => {
realtime.connect({ reconnect: true });
reconnectAttempt++;
}, delay);
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
reconnectAttempt = 0;
connectWithBackoff();
}
});
realtime.on('close', (e) => {
if (e.code !== 1000) connectWithBackoff(); // Don't backoff on intentional close
});
Escalation Criteria
Open a GitHub issue with the Appwrite team when all of the following apply:
- Server logs show repeated realtime worker OOM kills despite memory increases and replica scaling
- Close code 4000+ persists after SDK/server version alignment and token-refresh implementation
- Reconnection storms (>50 clients/sec) correlate with server CPU spikes (check
docker statsor monitoring) - Issue reproduces on Appwrite Cloud with default configuration (rules out infra misconfig)
Include in the issue: server version, SDK version, proxy config, relevant log snippets, and a minimal reproduction repo.
Limitations & Verification Checklist
- This guide covers v1.4.x–v1.5.x; v1.3 and earlier use a different Realtime protocol.
- Browser behavior varies—test on Chrome, Firefox, Safari (mobile + desktop).
- Network-level drops (cellular, corporate firewall) may require client-side offline queueing beyond Realtime scope.
Practical verification after fixes:
- Run
docker logs appwrite-realtime -f— confirm ping/pong lines every 30s for active client. - Simulate network drop:
tc qdisc add dev eth0 root netem loss 10%(on client or test VM) — observe reconnection within backoff window. - Background tab for 10 minutes, return — verify events resume without manual reload.
- Let JWT expire naturally — confirm auto-refresh and seamless reconnect.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.