Originally posted by eyalzek July 9, 2026
How are you running Renovate?
Self-hosted Renovate CLI (official renovate/renovate image) on Kubernetes.
Which platform are you running Renovate on?
GitHub.com
Which version of Renovate are you using?
43.251.3 (behavior observed consistently across 43.200.0 → 43.255.x)
Please tell us more about your question or problem
Summary: on a large monorepo, ~88% of the whole Renovate run's CPU goes into applyPackageRules — dominated by klona deep-clones via mergeChildConfig — executed once per matching rule per dependency instance. Because vulnerabilityAlerts compiles every open Dependabot alert into force package rules (with full advisory markdown in prBodyNotes) that join the per-dependency config, the effective cost scales roughly O(open alerts × package rules × dependency instances). On our repo this makes the lookup phase run for hours, and — worse than just being slow — the sustained synchronous clone/match churn starves the event loop for 60–90s at a stretch, so in-flight sockets get closed by peers (socket hang up on GraphQL POSTs, TLS handshakes timing out). A single such failure on a github datasource escalates to ExternalHostError, which aborts the whole repository run before any PRs are created.
Repo shape: monorepo with ~400 package files (~180 go.mod, ~200 package.json, plus Dockerfiles/workflows), i.e. tens of thousands of dependency instances (the same transitive Go dep appears in ~180 files and is fully re-processed per file). Several thousand open Dependabot alerts (large majority non-actionable transitive go-module advisories).
CPU profile (3-minute capture via inspector during the lookup phase, totals of self-time):
| self time |
share |
function |
location |
| ~120s |
66% |
klona |
klona/json via clone() |
| ~35s |
19% |
matchesRule |
util/package-rules |
| ~4s |
2.3% |
applyPackageRules |
util/package-rules |
| ~8s |
4.6% |
(garbage collector) |
|
Every hot klona call site has the same stack: applyPackageRules → mergeChildConfig → clone (klona).
Observed impact measurements:
- Disabling
vulnerabilityAlerts alone made dependency processing ~20× faster on this repo (same run otherwise: caches, node size, config) — consistent with the alert-derived rules being the biggest object in every clone.
- With alerts enabled, throughput was ~1 dependency-instance/second of pure local CPU (single core pinned, network mostly idle) — even for deps that end up
enabled: false and need no lookups at all.
- It's self-amplifying: when runs abort, no update/security PRs merge, so open alerts accumulate, so the per-dep cost grows, so the next run is slower. Our alert count roughly 2.5×'d in the weeks after runs started failing.
- Raising the heap (
--max-old-space-size) removed GC pressure (we measured mu = 0.006 when running against the default 4GB cap) but the event-loop stalls persist — it's compute, not memory.
Why it's fatal and not just slow: the multi-minute event-loop stalls kill unrelated in-flight requests; a failed POST /graphql (not retried by got) from github-tags/github-releases becomes ExternalHostError, and for onboarded repos lib/workers/repository/process/fetch.ts propagates that as a repo abort (external-host-error) with no config opt-out. So past a certain repo size × alert count, runs abort ~100% of the time.
Suggested directions (happy to help with a PR if maintainers agree on an approach):
- Memoize/deduplicate rule application per identical input — the same dependency (name, datasource, versioning, essentially identical file-level config) is fully re-evaluated for each of the ~180 package files that contain it. A cache keyed on the inputs of
applyPackageRules would cut our instance count ~two orders of magnitude.
- Avoid deep-cloning the full config per matching rule —
mergeChildConfig(config, rule) clones the entire accumulated config (which itself contains the complete packageRules array, including the alert-derived rules) once per matching rule. Options: strip packageRules (and other large, invariant keys) before the per-rule merge loop and reattach after; or accumulate matched rules and merge once.
- Keep advisory bodies out of the per-dependency config path — the alert-derived force rules embed full advisory markdown (
prBodyNotes) that is only needed at PR-creation time; storing bodies out-of-band and referencing them by id would shrink the cloned object dramatically for repos with many alerts.
I can share the full .cpuprofile and put together a synthetic public reproduction (N go.mod files sharing M deps + a large generated packageRules array) if that helps triage.
Logs (if relevant)
Representative sequence during a stall (log output drops from ~5 lines/s to ~1 line/20-30s, then all failures land in one burst):
DEBUG: Dependency: go.opentelemetry.io/otel/metric, is disabled (per-instance evaluations 20-30s apart)
...
DEBUG: POST https://api.github.com/graphql = (code=ECONNRESET, statusCode=-1 retryCount=0, duration=128272)
DEBUG: Shrinking GitHub GraphQL page size after error
WARN: Error obtaining docker token: Timeout awaiting 'request' for 60000ms
WARN: Host error (github-tags): Client network socket disconnected before secure TLS connection was established
INFO: Repository finished "result": "external-host-error"
```</div>
Discussed in #44444
Originally posted by eyalzek July 9, 2026
How are you running Renovate?
Self-hosted Renovate CLI (official
renovate/renovateimage) on Kubernetes.Which platform are you running Renovate on?
GitHub.com
Which version of Renovate are you using?
43.251.3 (behavior observed consistently across 43.200.0 → 43.255.x)
Please tell us more about your question or problem
Summary: on a large monorepo, ~88% of the whole Renovate run's CPU goes into
applyPackageRules— dominated byklonadeep-clones viamergeChildConfig— executed once per matching rule per dependency instance. BecausevulnerabilityAlertscompiles every open Dependabot alert into force package rules (with full advisory markdown inprBodyNotes) that join the per-dependency config, the effective cost scales roughly O(open alerts × package rules × dependency instances). On our repo this makes the lookup phase run for hours, and — worse than just being slow — the sustained synchronous clone/match churn starves the event loop for 60–90s at a stretch, so in-flight sockets get closed by peers (socket hang upon GraphQL POSTs, TLS handshakes timing out). A single such failure on a github datasource escalates toExternalHostError, which aborts the whole repository run before any PRs are created.Repo shape: monorepo with ~400 package files (~180
go.mod, ~200package.json, plus Dockerfiles/workflows), i.e. tens of thousands of dependency instances (the same transitive Go dep appears in ~180 files and is fully re-processed per file). Several thousand open Dependabot alerts (large majority non-actionable transitive go-module advisories).CPU profile (3-minute capture via inspector during the lookup phase, totals of self-time):
klonaklona/jsonviaclone()matchesRuleutil/package-rulesapplyPackageRulesutil/package-rulesEvery hot
klonacall site has the same stack:applyPackageRules → mergeChildConfig → clone (klona).Observed impact measurements:
vulnerabilityAlertsalone made dependency processing ~20× faster on this repo (same run otherwise: caches, node size, config) — consistent with the alert-derived rules being the biggest object in every clone.enabled: falseand need no lookups at all.--max-old-space-size) removed GC pressure (we measuredmu = 0.006when running against the default 4GB cap) but the event-loop stalls persist — it's compute, not memory.Why it's fatal and not just slow: the multi-minute event-loop stalls kill unrelated in-flight requests; a failed
POST /graphql(not retried by got) fromgithub-tags/github-releasesbecomesExternalHostError, and for onboarded reposlib/workers/repository/process/fetch.tspropagates that as a repo abort (external-host-error) with no config opt-out. So past a certain repo size × alert count, runs abort ~100% of the time.Suggested directions (happy to help with a PR if maintainers agree on an approach):
applyPackageRuleswould cut our instance count ~two orders of magnitude.mergeChildConfig(config, rule)clones the entire accumulated config (which itself contains the completepackageRulesarray, including the alert-derived rules) once per matching rule. Options: strippackageRules(and other large, invariant keys) before the per-rule merge loop and reattach after; or accumulate matched rules and merge once.prBodyNotes) that is only needed at PR-creation time; storing bodies out-of-band and referencing them by id would shrink the cloned object dramatically for repos with many alerts.I can share the full
.cpuprofileand put together a synthetic public reproduction (N go.mod files sharing M deps + a large generated packageRules array) if that helps triage.Logs (if relevant)
Representative sequence during a stall (log output drops from ~5 lines/s to ~1 line/20-30s, then all failures land in one burst):