Heading · flights.sahirm.com

What's eating the API quota

The alert is real, but it isn't LLM tokens and it isn't 75%. It's the AeroDataBox flight-data plan on RapidAPI, and it's at 87.5% with nine days left in the cycle. Measured live from RapidAPI's rate-limit headers on 29 Aug 2026, 18:34 UTC.

Where the plan actually stands

AeroDataBox BASIC, the $5 tier. Two independent caps: API units and request count. Only one of them is in trouble.

87.5%
of the monthly unit budget consumed
525 / 600
UNITS USED
75
UNITS LEFT
9.3 days
UNTIL RESET (~8 SEP)
~24 / day
BURN RATE · NEEDS ≤8
0150300450600 units
525 units spent in 21 days. At the current rate the plan runs dry around 2 September — six days early.

The trap: you're paying in units, not requests

Requests used: 497 of 2,400 (20.7%). Units used: 525 of 600 (87.5%). You are nowhere near the request cap and almost out of units — so "just make fewer calls" is only half the story, and the request counter you'd naturally watch is the one that looks healthy.

At ~1.06 units per request, essentially every call costs a unit. The unit cap is 4× tighter than the request cap, so the unit cap is the only one that matters. Budget against 600, not 2,400.

This is also not the first time. RapidAPI sent an 85% warning on 31 Jul and a 100%-exhausted notice on 2 Aug — the previous cycle was fully burned through with a week to spare. Today's 85% email is the same pattern repeating.

Estimated cost attribution — 525 units, 21 days

Single measure: units consumed this cycle. The first five bars are modelled from the code paths and the flight/subscription rows in the production database; the last is the residual. Only the total, the request count and the flight data are measured — see the evidence table below.

Hourly status poll
6 legs flown, T−6h→arr+2h
~95 units · 18%
Daily 04:30 reconcile
runs even with zero trips
~85 units · 16%
Codeshare double-billing
alias retried every tick
~50 units · 10%
Schedule-diff ladder
re-seeded on every deploy
~40 units · 8%
Subscribe + retries
3 exhausted × 6 attempts
~28 units · 5%
Unattributable
untagged call sites
~227 units · 43%

The biggest finding is that bar. Roughly 43% of the spend cannot be attributed to anything, because the app has no API metering. There is no usage table in the schema, completion.usage is never read, and the only accounting is a console.log that tags just three of the call sites — poll, schedule-diff and subscribe.

Everything else spends invisibly: the entire second AeroDataBox client in src/ingest/validate.ts (which bypasses adbFetch and runs on every parsed email, every confirm, every manual add and every paste import), plus all the balance, refill, delete and orphan-sweep calls. Fix the metering first — otherwise the next fix is a guess.

Measured evidence

WhatValueSource
Unit cap / remaining600 / 75x-ratelimit-api-units-*, live probe
Request cap / remaining2,400 / 1,903x-ratelimit-requests-*, live probe
Cycle reset801,486 s ≈ 8 Sepx-ratelimit-…-reset
Flights in the whole database19flights table, prod
Legs that flew this cycle6flights, dep ≥ 8 Aug
Active subscriptions2alert_subscriptions
Dead subscription rows35swept one-by-one, daily
Status events written, 21 days31 poll · 41 webhookflight_status_events
Reconcile runs21 (one per day)ops_log
Subscription retry exhaustions3 × 6 attemptsadb.subscription_retries_exhausted
Webhook credits left today15 of 50kv_state → adb.credits
Prior cycle outcome100% exhausted 2 AugRapidAPI email

Sanity check on the scale of the problem: 19 flights ever, 6 flown this cycle, 2 live subscriptions — and 525 units. That is ~87 units per flown leg. The plan allows about 8/day. Nothing here is user load; it's the machinery around the flights.

What to fix, in order

  1. Add real metering before anything else.unblocks the rest
    Route src/ingest/validate.ts through adbFetch, tag every call site, and persist a per-call row. Right now 43% of spend is invisible and every other fix is unmeasurable.
  2. Make the hourly poll subscription-aware.~95 units
    src/alerts/register.ts only skips a poll if a webhook landed in the last 75 minutes. A healthy subscription on an on-time flight sends nothing, so the skip never fires and the flight pays a unit an hour for 8–14 hours — for data the webhook would have pushed free.
  3. Stop double-billing codeshares.~50 units
    When the primary designator returns blank, the alias is fetched too — with no negative caching, so it re-fires every single tick. Seen live this cycle on LH1960 / AI4174.
  4. Scope the daily reconcile.~85 units
    The 04:30 job walks every active subscription and every non-terminal flight, then deletes provider orphans one at a time — against 35 dead subscription rows, every day, whether or not anyone is flying. Clean the dead rows and filter in SQL, not in JS after the fetch.
  5. Drop dateLocal from the subscription body.webhook credits
    AeroDataBox's schema rejects unknown fields, so it's silently ignored and the subscription matches every instance of that flight number. Each wrong-day push burns a webhook credit and is then discarded — which is why today's balance is 15 of 50.
  6. Short-circuit the LLM and validation on retry.OpenAI + ADB
    ingest.parse has retryLimit: 5 and never checks the already-persisted email.llmOutput, so one email can pay for extraction — and its AeroDataBox validation — up to six times.

Separately: the [positions] sampler is returning HTTP 403 on every call against airplanes.live. That one is free and keyless so it costs nothing, but the live-map feature it powers is silently dead — worth a look on its own merits.