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

  1. Wait until the cooldown expires before calling again (catch the error and retry after a delay)
  2. Reduce request frequency: cache scenes, back off polling intervals, or share one request across consumers
  3. Inspect the Retry-After header of the prior 429 to know the exact window; honor it in your scheduler
  4. 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

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


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)