# Performance measurement No production capacity, minimum resource requirement, SLO, alert threshold, pool size, or autoscaling threshold is known yet. The repository therefore contains a reproducible measurement profile, not a capacity claim or blocking resource preflight. ## Kind port-table memory investigation On 2026-07-20, the local kind control-plane container used 9.748 GiB while its four otherwise idle application `beam.smp` processes each had roughly 2.3 GiB RSS. Read-only runtime probes found an effective Erlang port limit of 134,217,727 and an allocated port table of 1,610,612,736 bytes in every kind BEAM VM. The nested runtime exposed `nofile=2,147,483,584`; OTP had derived the maximum port limit from that value. The same release image in ordinary Compose used the normal 65,536 port limit and a 786,432-byte port table. A separate VM probe with `ERL_ZFLAGS="+Q 65536"` reproduced those smaller values inside the kind pod before any rollout. Compose and Helm now set that value explicitly, and the startup and rolling-verification scripts check the effective value in every running web and worker VM. On a host with more CPUs than the intended server, the optional `compose.cpu-replay.yaml` override can constrain the CPUs visible to each long-running BEAM container: ```sh CPU_REPLAY_WEB_CPUS=1 CPU_REPLAY_WORKER_CPUS=1 \ CPU_REPLAY_WEB_SCHEDULERS=1 CPU_REPLAY_WORKER_SCHEDULERS=1 \ docker compose -f compose.yaml -f compose.cpu-replay.yaml up -d --wait ``` All four values are explicit experiment inputs. The override does not set a memory limit or represent a whole-server CPU budget. The scheduler inputs make total and online BEAM scheduler counts match the intended replay instead of leaving offline scheduler threads sized from the development host. None of these inputs is a production sizing recommendation. Running ordinary `docker compose up` without the override removes the replay constraints. After the Helm rollout, the unchanged kind control-plane container measured 1.796 GiB, a reduction of 7.952 GiB (81.6%). Its four application pod cgroups measured 188.5–215.6 MiB in the first post-change sample. A subsequent four-pod rolling replacement retained the 65,536 limit and 786,432-byte table; the replacement cgroups measured 194.3–211.4 MiB, all four replacement pods were Ready, cross-node PubSub passed, 184 readiness samples had no failed sample after retry handling, and the application-table count diff was empty. Evidence is retained under `output/memory/20260720-port-table-fix-005420` and `output/resilience/beam-port-limit-final-20260720`. In the ordinary 2-web/2-worker Compose profile, the immediate post-recreation sample measured 190.1–205.5 MiB per application container and 168.3 MiB for PostgreSQL. These local observations do not include a target server's OS and do not establish that a 1 GiB or 2 GiB machine is sufficient for production. On 2026-07-21, an isolated production-compiled compact Compose run measured 198.3 MiB for the combined Phoenix+Oban container and 70.9 MiB for its fresh PostgreSQL/PostGIS container in one no-load snapshot. An isolated split run against a separately started external-database fixture measured 207.2–219.6 MiB for each of two web and two worker containers, 20.05 MiB for the Docker API proxy, 41.66 MiB for Traefik, and 117.6 MiB for that database fixture. These are short workstation observations, not minimums, limits, or capacity claims. The profile uses a separate Compose project, generated independent secrets, and a separate PostgreSQL volume. Its Traefik instance is constrained to that exact Compose project; its router, service, middleware, and Host rule are unique, so the load profile cannot join the staging router. It exercises: - dynamic public home and Safety pages; - database-backed readiness queries; - Phoenix WebSocket upgrades and protocol heartbeats; - real password login and authenticated LiveView joins; - consent-driven tracking start, authenticated location writes, private chat writes, and explicit tracking stop/position deletion; - the configured number of web and worker replicas; - cross-node Phoenix PubSub after each run. It does not render a real browser, retain a browser tracking session for an unknown real-world trip duration, or reproduce an unknown future production traffic mix. It also does not sample the k6 load-generator container itself. Its results only describe the recorded host, container versions, replica counts, database state, and experiment inputs. ## Create the isolated profile ```sh ./scripts/ensure-local-load-env.sh ./scripts/load-stack-up.sh ``` The generated `.env.load` is ignored, restricted to mode 600, and contains independent PostgreSQL and application secrets. Edit its `LOAD_*` inputs to define a specific experiment. Values in `.env.load.example` are reproducible measurement points, not recommendations. For a one-run duration that must not rewrite `.env.load`, pass an explicit experiment override: ```sh LOAD_DURATION_OVERRIDE=10m ./scripts/load-run.sh local-soak-YYYYMMDD ``` The effective value and whether it came from `.env.load` or the override are recorded in that run's `environment.txt`. ## Run and compare replica counts ```sh ./scripts/load-run.sh two-web ./scripts/load-stack-up.sh 3 # Set LOAD_WEB_REPLICAS=3 in .env.load so the recorded expected topology # matches the running topology, then: ./scripts/load-run.sh three-web ``` Each run writes ignored evidence below `output/performance/