From 561d78a8d2a61a79edf363e73dade850c7216550 Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Fri, 27 Feb 2026 14:59:09 +0400 Subject: [PATCH] fix(wingbits): add 5-minute backoff on Wingbits /v1/flights failures When OpenSky is down, every theater posture poll falls through to Wingbits POST /v1/flights. Failed responses (429, 500, etc.) were not cached, causing retry-on-every-poll (~3 req/min) and sustained 429s. Add module-level backoff: any non-ok response or network error skips Wingbits calls for 5 minutes, falling through to stale/backup cache. Success resets the timer. Reduces Wingbits API load ~15x during OpenSky outages. --- .../military/v1/get-theater-posture.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/server/worldmonitor/military/v1/get-theater-posture.ts b/server/worldmonitor/military/v1/get-theater-posture.ts index 5f229dd2f..45a03c133 100644 --- a/server/worldmonitor/military/v1/get-theater-posture.ts +++ b/server/worldmonitor/military/v1/get-theater-posture.ts @@ -29,6 +29,11 @@ const BACKUP_TTL = 604800; // Flight fetching (OpenSky + Wingbits fallback) // ======================================================================== +// Backoff tracker: skip Wingbits calls for WINGBITS_BACKOFF_MS after a failure +// to avoid hammering the API with repeated 429s when OpenSky is down. +const WINGBITS_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes +let wingbitsBackoffUntil = 0; + function getRelayRequestHeaders(): Record { const headers: Record = { Accept: 'application/json', @@ -109,6 +114,10 @@ async function fetchMilitaryFlightsFromWingbits(): Promise { const apiKey = process.env.WINGBITS_API_KEY; if (!apiKey) return null; + if (Date.now() < wingbitsBackoffUntil) { + return null; + } + const areas = POSTURE_THEATERS.map((t) => ({ alias: t.id, by: 'box', @@ -126,7 +135,13 @@ async function fetchMilitaryFlightsFromWingbits(): Promise { body: JSON.stringify(areas), signal: AbortSignal.timeout(15_000), }); - if (!resp.ok) return null; + if (!resp.ok) { + console.warn(`[TheaterPosture] Wingbits ${resp.status} — backing off 5 min`); + wingbitsBackoffUntil = Date.now() + WINGBITS_BACKOFF_MS; + return null; + } + + wingbitsBackoffUntil = 0; const data = (await resp.json()) as Array<{ flights?: Array> }>; const flights: RawFlight[] = []; @@ -154,6 +169,7 @@ async function fetchMilitaryFlightsFromWingbits(): Promise { } return flights; } catch { + wingbitsBackoffUntil = Date.now() + WINGBITS_BACKOFF_MS; return null; } }