Forgejo OOMKilled repeatedly: AI crawler enumerating issue-filter URL space, ingress rate limit structurally blind #938
Labels
No labels
burndown-2026-06
burndown-2026-08
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/infrastructure#938
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
Intermittent 503s from forgejo.coilysiren.me, reported 2026-08-26 ~01:44 UTC.
Root cause
forgejo-6fb99c98fc-8hsfp(single replica,strategy: Recreate) is being OOMKilled. Every kill produces a 503 window until the readiness probe passes again.Evidence from
kubectl describe pod:Last State: Terminated-Reason: OOMKilled-Exit Code: 13718:15:38 -> 18:36:21 -0700, so 21 minutesRestart Count: 3over the pod's 30h life, and acceleratingContainer memory sampled live while writing this: 892 MB at 01:44:21 UTC, 975 MB at ~01:47:30 UTC, so roughly 26 MB/min of sustained climb after startup against a 3Gi limit.
What is driving it
An AI/scraper fleet is enumerating the combinatorial issue-filter URL space. Sample from a single 0.3 second window in the container that died:
Note the 347 second response times. Requests pile up, each holding memory, until the cgroup limit is hit.
Rate of
repo.Issueshits per 10 minutes over the last 6h: baseline ~2,000-3,600, ramping from ~00:40 UTC to a peak of 28,883 per 10 minutes (~48/s).Traefik access logs give attribution over a 2h window on
forgejo.coilysiren.me:forgejo-runner/v12.13.2- 44,712 (legitimate)meta-externalagent/1.1- 4,251 + 2,200git/2.52.0- 952Eight distinct "browser" agents landing within 3 percent of each other is not organic traffic. That is one scraper fleet rotating UA strings, roughly 145k requests / 2h (~20/s), plus ~6.4k that openly declare Meta's crawler.
Why the existing guard did not fire
Middleware/forgejo-ratelimitis attached to the router and readsaverage: 50, burst: 100, period: 1s.svclb-traefikpod IP10.42.0.93. Forgejo logs every single client as10.42.0.93:0, and Traefik'sClientHostis10.42.0.93for all of them. So the per-source rate limiter buckets the crawler, the runners, and real users into one shared bucket. It cannot distinguish clients at all, which also means tightening the number alone would throttle the runners first./*/*/src/,/*q=*,/*sort=*,/*tab=*, but not/*/*/issues?...or/*/*/pulls?...filtered bylabels=,assignee=,milestone=,state=,poster=,project=,type=. Those are exactly the URLs being walked. A UA-rotating scraper likely ignores robots.txt regardless.Separate landmine: memory limit drift
The live deployment and the tracked manifest disagree.
Limits.memory- 3Gideploy/forgejo.yml- 1500MiCPU and ephemeral-storage match, memory does not. Forgejo is being OOMKilled at 3Gi today. The next Ansible converge would reset it to 1500Mi and cut time-to-OOM by more than half. This should be reconciled deliberately, in whichever direction, rather than discovered during the next incident.
Not yet done
No live change has been made. Everything above is read-only observation. The ingress-filtering decision (block by UA, fix source-IP visibility then tighten the limit, extend robots.txt, or some combination) is a public-surface change and is with Kai.
Candidate remediations, roughly in order of leverage:
externalTrafficPolicy: Localon the Traefik service (or otherwise preserve client IP) so the rate limiter and any block rule can actually see distinct sources. This is the prerequisite for 2 being meaningful.forgejo-ratelimitonce source IPs are real, and consider a separate tighter limit on theissues/pullsfilter paths.Filed by the sysadmin seat during live diagnosis.
Duplicate of #909, which was filed 2026-08-24 for the same crawler and the same OOM and is still open at P0. I filed this before finding it.
Fresh evidence from tonight's recurrence has been added to #909 as a comment, and the guard is authored in #939. Closing here so the record stays in one place.
Two things in this issue's description were wrong and are corrected in #909, noting them so nobody acts on this text:
externalTrafficPolicy: Localto restore client attribution. That was already applied on 2026-08-24 and does not work, because klipper-lb SNATs through a pod network namespace before Traefik sees the packet.