Rate Limits

PreflightAPI enforces two types of throttling: rate limits (requests per 60-second window) and monthly quotas (total calls per billing period). Both depend on your subscription plan.

Limits by Plan

PlanRate LimitMonthly Quota
Student Pilot10 requests / 60 sec5,000 calls
Private Pilot60 requests / 60 sec150,000 calls
Commercial Pilot300 requests / 60 sec750,000 calls

Rate limits are enforced on a sliding 60-second window per subscription key. If you exceed the limit, further requests in that window are rejected with 429 Too Many Requests until the window resets.

Monthly Quotas

In addition to per-minute rate limits, each plan has a monthly quota that caps the total number of API calls in a billing period. Quota counters reset at the start of each monthly billing cycle.

  • When you hit your monthly quota, all further requests return 429 Too Many Requests with a QuotaExceeded error until the quota resets.
  • You can track your current usage on the dashboard overview page.
  • Upgrading your plan immediately increases both your rate limit and monthly quota.

Rate Limit Headers

Every API response includes headers that let you monitor your rate limit usage in real time:

HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
HeaderDescriptionPresent On
X-RateLimit-LimitMaximum requests allowed in the current 60-second windowEvery response
X-RateLimit-RemainingRequests remaining before you hit the rate limitEvery response
Retry-AfterSeconds to wait before retrying429 responses only

Data Currency Headers

In addition to rate limit headers, every successful response from a data endpoint includes data currency headers (X-Data-Currency, X-Data-Last-Updated, X-Data-Sync-Age-Minutes) that indicate how current the underlying data is. See the data currency guide for details on staleness detection and severity levels.

Exceeding Limits

Both rate limit and quota errors return 429 Too Many Requests. You can distinguish them by the error field in the response body.

Rate limit exceeded (429)

When you exceed your per-minute rate limit, the API returns 429 Too Many Requests with a standard Retry-After header and a retryAfterSeconds field in the body:

{
  "error": "RateLimitExceeded",
  "message": "Too many requests. Please slow down and try again shortly.",
  "retryAfterSeconds": 45
}

Monthly quota exceeded (429)

When you exhaust your monthly quota, the API returns 429 Too Many Requests with a quotaResetsAt timestamp indicating when your quota renews:

{
  "error": "QuotaExceeded",
  "message": "You have reached your monthly API call limit.",
  "quotaResetsAt": "2026-03-15T06:00:00.0000000Z"
}

The quotaResetsAt value is an ISO 8601 UTC timestamp. The quota resets at the start of your next billing cycle.

Check the error field to distinguish rate-limit (RateLimitExceeded) from quota (QuotaExceeded) responses. See the error handling guide for details on all error formats.

Response Caching

GET responses are cached at the API gateway to reduce latency. Cache duration varies by data type. Cached responses are identical to fresh responses and still count toward your rate limit and monthly quota.

Endpoint CategoryCache Duration
Real-time weather (METARs, PIREPs)2 minutes
E6B calculations (live METAR mode)2 minutes
Forecasts (TAFs, SIGMETs, G-AIRMETs)5 minutes
NOTAMs5 minutes
Winds aloft5 minutes
Presigned URLs (terminal procedures, chart supplements)10 minutes
Static / NASR data (airports, frequencies, airspace, obstacles, NAVAIDs)15 minutes

Only GET requests are cached. POST endpoints are never cached.

Tip

If your HTTP client supports cache TTLs (e.g. staleTime in TanStack Query), match them to these cache durations for optimal freshness without redundant requests.

Monitoring Your Usage

  • Dashboard — The dashboard overview shows your current monthly usage and remaining quota at a glance.
  • Response headers — Check X-RateLimit-Remaining after each request to track your real-time rate limit usage.
  • Plan ahead — If you're consistently hitting your limits, consider upgrading your plan for higher throughput.

Best Practices

  • Cache locally — Store responses on your side to avoid redundant requests. Match the cache TTL to the gateway cache duration for optimal freshness.
  • Use exponential backoff — When you receive a 429, wait for the Retry-After duration before retrying. Use exponential backoff with jitter to avoid thundering herds.
  • Monitor headers — Check X-RateLimit-Remaining to proactively slow down before hitting the rate limit.
  • Batch where possible — Some endpoints accept multiple identifiers in a single call (e.g., fetching METARs for multiple ICAO codes). Use these to reduce the number of requests.

Retry with Exponential Backoff

Here's a reusable fetch wrapper that automatically retries on 429 responses with exponential backoff and jitter:

async function fetchWithRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3,
): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options)

    if (response.status !== 429) {
      return response
    }

    // Check if this is a quota error (not retryable)
    const body = await response.clone().json()
    if (body.error === 'QuotaExceeded') {
      throw new Error(`Monthly quota exceeded. Resets at ${body.quotaResetsAt}`)
    }

    if (attempt === maxRetries) {
      throw new Error('Rate limit exceeded after max retries')
    }

    // Use Retry-After header or retryAfterSeconds from body
    const retryAfter = response.headers.get('Retry-After')
    const baseDelay = retryAfter
      ? parseInt(retryAfter, 10) * 1000
      : (body.retryAfterSeconds ?? Math.pow(2, attempt)) * 1000

    // Add random jitter (0-500ms) to prevent thundering herd
    const jitter = Math.random() * 500
    await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter))
  }

  throw new Error('Unreachable')
}

// Usage
const response = await fetchWithRetry(
  'https://api.preflightapi.io/api/v1/metars/KJFK',
  {
    headers: {
      'Ocp-Apim-Subscription-Key': process.env.PREFLIGHT_API_KEY!,
    },
  },
)
const data: Metar = await response.json()

Search Documentation

Search docs, endpoints, and schemas