Crossplane · 03 of 04 · Pollers and loops

Three ways to empty a 5,000-call bucket

Every reconcile spends someone's API budget. On GitHub mine was 5,000 calls an hour, shared with Backstage, and I emptied it three different ways before I learned to count.

Three ways to empty a 5,000-call bucket Three panels drain one shared GitHub API budget of 5,000 calls an hour per account, shared with Backstage. Background polling (2026-09-07): provider-github polls every Repository on a ten-minute default, forever; fixed by polling hourly and capping the reconcile rate. Annotation fight (2026-09-23): the composition renders the external name repo:file: while the provider rewrites it to repo:file:main every two seconds, a loop costing about 280 calls a minute; fixed by rendering the observed name. Retry loop (2026-09-22): a template default change from Dockerfile to Containerfile hits a force-replace field, and the provider retries at a sixty-second backoff that ignores the poll interval, about 16,000 calls an hour from seven objects; fixed by pinning the file and name to what the provider observed. BACKGROUND POLLING · 09-07ANNOTATION FIGHT · 09-23RETRY LOOP · 09-22provider-github pollsdefault --poll=10m, every RepositoryBackground drainforever, changed or notFIX · 09-07Poll hourly, cap the rateRenders repo:file:the name the template computesProvider writes repo:file:mainits own id, every ~2sFIX · 09-23Render the observed nameTemplate default changesDockerfile to ContainerfileRetries at 60s backoffforce-replace field; --poll ignoredFIX · 09-23Pin file and nameALWAYS ON~280 A MINUTE~16,000 AN HOURGitHub API budget5,000 calls an hour per account, shared with BackstageNone of these was a Crossplane bug. Each was me not yet knowing what a reconcile costs.LEGENDThe loopCause or fixShared budgetSpends calls

Polling is a cost

The provider's default poll is ten minutes, per managed resource, forever. Every new cluster from the Apron template now starts with --poll=1h and --max-reconcile-rate=50.

Never fight the provider

If the provider owns a field, like the external name it assigns, render back exactly what it wrote. Two writers with different spellings of one name is a loop, not a disagreement.

The worst one

The provider release I ran read a rate-limit 403 as "this file was deleted" and re-created it, overwriting real values.yaml content. An upstream fix already existed but hadn't been released, so I built and published my fork's main.

Hangar · Airframe · Crossplane