The amadan-side multiplier (eager, permanent, unrecoverable repo instances) is amadan/amadan discussion #19: https://amadan.net/amadan/amadan/discussions/19
Flagship went fully dark on memory pressure (2026-08-28): one tenant can take the box, and nothing notices Issue open
What happened (2026-08-28)
The flagship (carlos-flagship-ie, i-092c0c1eea75723cb, then a t4g.small: 1.86 GB, no swap) went completely dark from ~11:03 to 11:18 UTC — amadan.net, console.carloku.com / platform.carlosframework.com, keymail.dev and home.kass.training all together. TCP SYNs to 80/443 went unanswered; ICMP too.
The box was not dead. It was thrashing:
- 09:00–09:50 inbound traffic roughly doubled (modest; mostly the usual scanner/ACME noise, nothing that looks like an attack).
- 11:01:52 the kernel starts logging
systemd-journald: Under memory pressure, flushing cachesand never stops. - 11:03
NetworkPacketsIncollapses from ~300k/5min to ~1k. The box is no longer answering anything. - The OOM killer never fired. With no swap the kernel had nothing to evict but page cache, so it flushed the same caches in a loop instead of killing a process.
- EC2 status checks stayed
ok/okthroughout, so theStatusCheckFailed_Systemauto-recover alarm (the only alarm the box has) could never fire. SSMsend-commandsat Pending. Nothing paged anyone.
Recovery was a manual reboot (11:18) and, an hour later, a resize to t4g.medium. The resize buys headroom; it does not change the failure mode.
Why the box could hang
Five minutes after a clean boot the box already sat at ~1.0 GB used: the amadan hub spawns one amadan-hub repo child per repo (25 of them, ~47 MB RSS each ≈ 1.2 GB) plus ~10 keymail instances. All of it lives in the delegated carlos-edge.service/carlos-exec@<host> cgroups, and carlos-edge.service has MemoryMax=infinity — unlike carlos-app@, which caps at 512M. So one tenant's growth was free to consume the box, and there was no swap to turn the cliff into a slope.
The tenant-side multiplier (amadan's eager, permanent repo instances) is filed on amadan/amadan separately. This issue is about what the platform should guarantee: one tenant cannot take the box, and a box in trouble is noticed.
Proposed mitigations (platform side)
Ordered so each one leaves the box safer on its own.
1. zram swap on every region-host box
systemd-zram-generator with zram-size = ram / 2, zstd, vm.swappiness=100. Compressed RAM, so it fits a root disk that is already 85% full (2.9 GB free on the flagship). This is the single change that converts "kernel thrashes forever" into "things get slow, then the OOM killer does its job".
2. A memory ceiling on the edge tree, then per-app caps
- Now: a drop-in on
carlos-edge.service—MemoryHigh=80%,MemoryMax=90%. Past High the kernel throttles the tree; at Max it kills the largest process in the cgroup, which is always a tenant process, never the agent. The activator already respawns a dead exec instance on its next request (internal/activator/activator.go:783), so a killed app comes back on demand. Worth akill -9on the box to confirm before relying on it. - Properly:
internal/cgroup.Manager.Startalready createscarlos-exec@<host>; it should also writememory.max/memory.highthere from a per-app value, defaulting to the 512Mcarlos-app@already enforces. Then amadan blowing up kills amadan, not keymail. The drop-in stays as the outer wall.
3. Detection
- A Route53 health check on
https://amadan.net/and onconsole.carloku.com→ CloudWatch alarm → an SNSalertstopic with an email subscription (same shape as the existingdeliverabilitytopic inflagship/mail.tf). Would have fired at ~11:04 today. - Memory itself never leaves the box. Smallest path: a one-minute
carlos-memwatch.timerdoingaws cloudwatch put-metric-datawithMemAvailableand PSImemory some avg60,cloudwatch:PutMetricDataon the region-host role, and an alarm atMemAvailable < 15% for 3 min. That is the early warning the health check cannot give.
4. Write down what is on the box
host-init.sh never re-runs (user_data_replace_on_change = false) and there is no config management, so today's live box is partly hand-built over SSM with nothing recording it (amadan-backup.timer is on the flagship from an unmerged fleet branch, for example). Proposal: an idempotent flagship/harden-box.sh in platform-infrastructure that is the only way changes go onto a live box, with the same steps added to host-init.sh for new boxes. Also region-ie.tf still says t4g.small; the box is a t4g.medium since 11:27 UTC today.
Not proposed
- Raising
OOMScoreAdjuston amadan's repo children — a dead repo instance stays dead until the hub restarts, so killing a child is worse than killing the hub. - Moving amadan to its own fleet box — right answer long term, still blocked on platform #63/#73 and a dedicated account.
Evidence
CloudWatch NetworkPacketsIn/CPUUtilization for i-092c0c1eea75723cb 09:00–11:20 UTC; aws ec2 get-console-output from the same window; journalctl -b -1 on the box (no oom-kill lines, memory-pressure lines from 11:01:52 to shutdown); systemctl cat carlos-edge.service (MemoryMax=infinity, Delegate=cpu memory io pids); systemd-cgls -u carlos-edge.service showing the 25 repo children under carlos-exec@amadan.net.
Built, reviewed, not yet merged or applied — platform PR carlosframework/platform#336 and platform-infrastructure#28. Spec and plan: platform-design/superpowers/{specs,plans}/2026-08-28-memory-pressure-containment* on the pinfra branch.
What the box says now (read-only SSM, after the resize) — four corrections to the write-up above:
carlos-exec@amadan.netis 257 MB anonymous across the 25 repo children (595 MB with page cache). The 47 MB "each" is RSS, which counts shared pages 25 times.- The agent's own leaf (
supervise/) is charged 1.2 GB, 1.12 GB of it page cache from release-store reads — reclaimable, and the first thing any ceiling on the tree reclaims. - keymail is
keymail-relay.service(343 MB), an ordinary unit; it is not under the edge tree. The tree has two leaves. flagship/region-ie.tfstill saidt4g.small: a plaintofu applywould have resized the box back down. Fixed on the branch.
What's built (all four mitigations):
- zram via
scripts/harden-box.sh <profile> <instance>(idempotent, SSM) + the same lines in everyhost-init.sh/.tftpl. ram/2, zstd, swappiness 180, page-cluster 0. carlos-edge.service:MemoryHigh=70%,MemoryMax=80%— in the embedded unit file, not a drop-in, so it rolls with the binary. PlusOOMPolicy=continue, which the proposal misses: systemd's defaultstopturns one tenant's OOM kill into a whole-edge restart. And, from review: the memcg OOM killer scores per process by RSS, so the outer wall's first victim would have been the agent (110 MB vs 20–60 MB tenant processes) — every exec child now getsoom_score_adj=+500at spawn.cgroup.Manager.Startwritesmemory.max/memory.high/memory.oom.grouppercarlos-exec@<host>: 512M default, 1G when the manifest declarescapabilities.children. amadan-hub declares nothing today, so amadan needs"capabilities": {"children": true}in its manifest before the platform rolls, or it lands on 512M — the cap bounds total charge (anon + cache), and 595 MB under a 512M cap is permanent reclaim.- Detection: Route53 checks on amadan.net + console.carloku.com → alarms → email; and a
memwatchagent duty (not a shell timer) publishingMemAvailablePercent+ PSIsome/avg60to CloudWatchCarlos/Hostonce a minute,MemAvailablePercent < 15% for 3 minalarm per box, missing data = breaching. Alarms and their SNS topics are per region (an alarm cannot notify a cross-region topic), and the memory alarms are wired in a second apply once every box publishes.
Rollout order is spec §4 and is load-bearing; nothing has touched the live box.
Deployed 2026-08-28 (Paul: "merge and deploy"). platform #336 + #342 + #343 merged, binary 0ac8c98 rolled to all four flagship boxes and both dev boxes; pinfra #28 merged and applied (A then B).
Live on the flagship now: zram (1.9 GB on IE, ~930 MB on the t4g.smalls), carlos-edge at MemoryHigh 70% / MemoryMax 80% / OOMPolicy=continue, every exec child at oomscoreadj +500, carlos-exec@amadan.net capped at 1G (amadan-hub 8a866cd-mem ships capabilities.children), memwatch publishing Carlos/Host per region, memory-low alarms OK on IE/US/AU (UK still evaluating), reachability alarms on amadan.net + console.carloku.com OK.
Three things the rollout taught, none in the spec's first draft:
- With zram on the box,
memory.maxalone is escapable — a 200 MB allocator under a 64M cap just swapped.memory.swap.max(= max) is now written alongside it (#342), proven withsystemd-run -p MemoryMax=64M -p MemorySwapMax=64M→ killed. - host.env's fleet-wide
AWS_REGION=eu-west-1sent every box's metrics to Ireland while each box's alarm lives in its own region. memwatch now publishes to the IMDS placement region (#343). kill -9of an amadan repo child leaves the edge untouched (NRestarts 0, amadan.net 200) — and the child stays dead, which is amadan #19's half.
Remaining: dev's tofu root can't be applied (DNSimple record drift, #319), so dev boxes have zram + the binary but no IAM grant/alarm; tito boxes are tito's roll.
Status check, three days on. Everything in the write-up above is merged and applied; what is left is two box classes that the flagship roll could not reach.
Verified now:
- platform #336 (2026-08-28 16:48), #342 (17:10), #343 (17:19) — all MERGED.
- platform-infrastructure #28 "Memory pressure: zram, alerts, memory alarms, region-ie.tf truth" — MERGED 17:24.
- The flagship serves
x-carlos-version: 5ef69c6, which is well after 0ac8c98, so the binary-embedded half is live there:carlos-edgeat MemoryHigh 70% / MemoryMax 80% / OOMPolicy=continue, exec children at oomscoreadj +500, per-carlos-exec@<host>memory.max/high/oom.group and memory.swap.max, memwatch publishing toCarlos/Host. - Tito serves
x-carlos-version: c3f021d(2026-08-29, PR #355), and 0ac8c98 is an ancestor of it. So the tito boxes have the binary half too, without anyone having to do anything for it — which is the point of putting those limits in the embedded unit file rather than a drop-in.
What is NOT done, and the split matters because the two halves protect against different things:
- dev boxes have zram and the binary but no IAM grant and no alarm — dev's tofu root cannot be applied because of DNSimple record drift. Tracked as platform #319, still OPEN.
- tito boxes have the binary half and, as far as I can tell, none of the account-side half: no zram, no
Carlos/Hostalarms, no Route53 reachability checks. Those live in the tito account's own tofu and inscripts/harden-box.sh, so a flagship apply cannot reach them; it needs a tito-side apply. I can find nothing tracking it — not in teamtito/carlos-infrastructure (one open issue, #8, about the region fleet), not in platform, not in platform-infrastructure.
Item 2 is the one worth a decision. The binary half caps a tenant and stops the OOM killer picking the agent, which is most of the original failure. But zram is the change that turns "kernel thrashes forever with nothing to reclaim" into "things get slow, then something gets killed" — the actual mechanism that took the flagship dark for 15 minutes — and the alarms are the reason anyone would find out. A tito box under the same pressure today would still go quiet without paging anyone.
Happy to file that against teamtito/carlos-infrastructure and write the tito profile into harden-box.sh, but it is a tito-side apply and by the standing rule that roll is tito's to ask for, so I have not.
Leaving this open on account of the two above rather than closing it as deployed.
Triage, 2026-09-05. Nothing here has moved since the three-days-on status check, and the two remaining items are both blocked on somebody else's apply rather than on any work in this repo.
Restating what is left, because the split is what makes this still worth having open:
- dev boxes — zram and the binary half, no IAM grant and no alarm, because dev's tofu root cannot be applied over DNSimple record drift.
- tito boxes — the binary half only. No zram, no
Carlos/Hostalarms, no Route 53 reachability checks. Those live in the tito account's own tofu and inscripts/harden-box.sh, so no flagship apply reaches them.
Item 2 is the one that matters and the reasoning above is right: the binary half caps a tenant and stops the OOM killer picking the agent, which is most of the original failure, but zram is the change that addresses the actual mechanism that took the flagship dark for fifteen minutes. With no swap the kernel had nothing to evict but page cache and flushed it in a loop instead of killing anything; the OOM killer never fired. A tito box under the same pressure today still has nothing to reclaim, and no alarm to say so.
My recommendation is to take the offer that was already made. Writing the tito profile into harden-box.sh and filing against teamtito/carlos-infrastructure costs little and is not a roll — it puts a reviewed, idempotent script and a tracked issue in front of the people whose apply it is. Holding back the preparation because the apply is theirs is stricter than the standing rule needs to be, and the current state is that nothing anywhere tracks it: not in teamtito/carlos-infrastructure, not in platform, not in platform-infrastructure. An untracked gap on a box class that can go dark is worse than an unwelcome issue.
Item 1 stays parked behind the DNSimple drift, which is its own piece of work.
Keeping open. The incident is closed and the flagship is fixed; this is now a coverage gap on two box classes, and it would be reasonable to retitle it as such so the open item is not mistaken for an unresolved outage.
Blocked on: your say-so to prepare the tito-side change and file it.