koala73/worldmonitor · warning
Imagery search rate limited
Error message
Imagery search rate limited
What it means
fetchImageryScenes() keeps a module-level cooldown (retryAfterUntil) set when a previous request got HTTP 429. While that deadline is in the future, it throws immediately without a network call, honoring the API's Retry-After so clients stop hammering the imagery search endpoint.
Solutions
- Wait until the cooldown expires before calling again (catch the error and retry after a delay)
- Reduce request frequency: cache scenes, back off polling intervals, or share one request across consumers
- Inspect the Retry-After header of the prior 429 to know the exact window; honor it in your scheduler
- If needed persistently, route requests through your own proxy with rate-limit budgeting
Example fix
// before
const scenes = await fetchImageryScenes(params);
// after
async function fetchWithBackoff(params) {
try { return await fetchImageryScenes(params); }
catch (e) {
if (e.message === 'Imagery search rate limited') {
await new Promise(r => setTimeout(r, 30_000));
return fetchImageryScenes(params);
}
throw e;
}
} Defensive patterns
Strategy: retry
Try / catch
try { scenes = await fetchImageryScenes(p); } catch (e) { if (e.message === 'Imagery search rate limited') { await sleep(30_000); scenes = await fetchImageryScenes(p); } else throw e; } Prevention
- Throttle imagery polling below the API rate limit
- Debounce/dedupe bbox-based searches across components
- Honor Retry-After from 429 responses in your scheduler
When it happens
Trigger: Calling fetchImageryScenes() (or the imagery scenes UI) after an earlier request returned 429 while the Retry-After cooldown window is still active.
Common situations: Auto-refreshing map panels polling imagery search faster than the rate limit; many components sharing the module-level limiter and one 429 blocks all of them; a server Retry-After of 60s (default) while a dashboard keeps retrying on a timer.
Related errors
- Scenario queue is at capacity, please try again later
- Too many recent submissions for this email; try again later.
- Exa ${endpoint.slice(1)} failed HTTP ${response.status}: ${d
- Revoke failed (HTTP ${resp.status}).
- Firecrawl extract error: ${data.error ?? 'unknown'}
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/b0136e78fee0c14e.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/imagery.ts:16
import { toApiUrl } from '@/services/runtime';
import type { ImageryScene } from '@/generated/server/worldmonitor/imagery/v1/service_server';
export type { ImageryScene };
export interface ImagerySearchParams {
bbox: string;
datetime?: string;
source?: string;
limit?: number;
}
let retryAfterUntil = 0;
export async function fetchImageryScenes(params: ImagerySearchParams): Promise<ImageryScene[]> {
if (Date.now() < retryAfterUntil) throw new Error('Imagery search rate limited');
const url = new URL(toApiUrl('/api/imagery/v1/search-imagery'), window.location.origin);
url.searchParams.set('bbox', params.bbox);
if (params.datetime) url.searchParams.set('datetime', params.datetime);
if (params.source) url.searchParams.set('source', params.source);
if (params.limit) url.searchParams.set('limit', String(params.limit));
const resp = await fetch(url.toString(), { signal: AbortSignal.timeout(15_000) });
if (!resp.ok) {
if (resp.status === 429) {
const retryAfter = resp.headers.get('Retry-After');
const seconds = retryAfter === null ? NaN : Number(retryAfter);
const deadline = Number.isFinite(seconds) ? Date.now() + Math.max(0, seconds) * 1000 : Date.parse(retryAfter ?? '');
retryAfterUntil = Number.isFinite(deadline) ? deadline : Date.now() + 60_000;
}
throw new Error(`Imagery search failed: ${resp.status}`);
}
const data = await resp.json();
return data.scenes ?? [];View on GitHub (pinned to 7d06c8633d)