Plan: 100 thousand users on one VPS, CDN-ready

Document version 2026-05-29 · Goal: the current single VPS should hold up to 100k MAU without falling over, and the app should move to a CDN seamlessly when the time comes.

Target numbers

MetricValueSource
MAU100,000the goal
DAU30,00030% of MAU, normal for a utility app
Concurrent WS10,000 (peak 15k)~10% of MAU during the day, 15% at peak
Actions per day90,0003 openings or calls per daily user
HTTP API requests per second (average)3-5mostly key_create, host_sync and crash_report
HTTP API requests per second (peak)30-50after a mass release that updates numbers

The architecture today

Phase A — the Android client NOW, v6.66

Goal: cut the number of redundant requests clients generate, so the server has less to do.

A1. Exponential HTTP retry backoff for pending host keys

File: HostService.kt:1424-1432. Today syncPendingHostKeys() is called every 60 s and hits Api.keyCreate for every pending key with no backoff.

Store in Prefs per-localId: attempt_at_id and attempt_count_id. Backoff: 30 s → 60 s → 2 min → 5 min → 15 min → 30 min (capped). Reset on success.

A2. HTTP retry backoff for pending guest bundles

File: Vault.kt:213. The same logic.

A3. overridesVersion compare-before-bump

File: GuestConn.kt (5+ places). Compare the new parseNumbers with the previous one, by hash or directly. If they are identical, do not bump AppState.overridesVersion. This cuts the recompose waves — 80 SharedPreferences reads × 20 cards — on every WebSocket initialisation.

Phase B — server protection and CDN readiness NOW

Goal: protect the server from a storm even when a client misbehaves, and detach the APK address from the domain.

B1. A rate limit on host_hello and guest_hello

In server.php case 'host_hello' / 'guest_hello': if the same device_id / user_key sent a hello less than N seconds ago, close the connection without touching the database. N = 5 s.

Protection in case a client with broken logic sends a hundred hellos a second.

B2. Cap pending_host_msgs per host_id

In enqueueHostMsg: if this host_id already has more than 200 rows, delete the oldest with DELETE FROM pending_host_msgs WHERE host_id=? ORDER BY id ASC LIMIT 1. Protection against endless queue growth under malicious activity.

B3. CDN readiness: move the APK URL into the config

In _config.php:

$GLOBALS['apk_url'] = 'https://entrixy.com/entrixy.apk';
$GLOBALS['download_page_url'] = 'https://entrixy.com/download';

In api/version_check.php: 'download_url' => $GLOBALS['apk_url'].

When the move to a CDN happens, exactly one line changes in _config.php to https://apk.entrixy.com/entrixy.apk (Cloudflare R2 with a custom domain). No app release involved.

B4. The APK on a CDN — integrity checking

The client already verifies the APK signature on download (debug keystore, see build.gradle.kts:23-29). That is enough for a CDN: even if a CDN node swaps the file, the signature will not match and Android refuses to install it. No extra work is needed on the client.

Phase C — scaling Workerman out WHEN WE HIT THE CEILING

The current count=1 Workerman holds around 10k concurrent WebSockets without trouble — server.php line 40 already starts with it. The first bottleneck will be PHP's CPU time during peak message exchange: 10k pongs a second plus the actions.

C1. Workerman count=2–4 with Redis pub/sub for shared state

Today $hosts, $guests, $calls are in-memory arrays inside a single process. To run several processes they have to move into Redis:

When a message arrives at worker W1 but the recipient is on W2, relay it through Redis pub/sub.

Effect: linear scale-out up to the VPS core count. On four cores that is roughly 40k concurrent.

C2. An alternative: one machine, nginx or HAProxy in front of Workerman, sharded by client_ip

Less flexible and needs no Redis. Each user always lands on the same worker. But it breaks down when sender and recipient end up on different workers.

Phase D — when to move to a CDN LATER

  1. A Cloudflare R2 bucket → upload entrixy.apk.
  2. Custom domain apk.entrixy.com → connect it through the Cloudflare CDN.
  3. In _config.php change $apk_url to the new one.
  4. In the deploy script, after assembleRelease add r2 cp app-release.apk apk-bucket/entrixy.apk.

What NOT to do now

What is being done right now

  1. Phase A in full → release v6.66.
  2. Phase B in full, without restarting the WebSocket server, for testing before confirmation.
  3. Phase C is done in a separate branch, needs Redis on the VPS and is a task of its own.
  4. Phase D is documented above and executed when needed.