Caching & monitoring

METAR/TAF 404 fallbacks: Error handling. Platform HTTP errors and retries: Error Handling.

Data freshness

Data typeUpdate scheduleNotes
METAREvery 30 min (major airports), every 60 min (smaller stations)SPECI reports go out immediately on significant condition changes
TAF4 times per day - 00Z, 06Z, 12Z, 18ZValid for 24 or 30 hours; a TAF is still current between issuances
Winds Aloft (FB)Fixed FAA product scheduleUS only; valid_time on response marks forecast validity
PIREPsContinuous filing by pilotsEphemeral - use a short look-back (hours param)
AIRMET/SIGMETIssued and updated on FAA scheduleExpire client-side using start_time / end_time

The timestamp field on every response is when the API fetched the data - it changes on every request even if the underlying observation hasn't changed. For METAR age, use parsed.time (observation time from the bulletin header). For TAF, do not treat parsed.start_time as bulletin recency - it marks the start of the valid forecast window. Between the four daily issuances the same TAF remains current; poll at most every 15–30 minutes or after expected issuance times (00Z, 06Z, 12Z, 18Z).

import requests
from datetime import datetime, timezone

HEADERS = {
    "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"


def parse_observation_time(iso: str) -> datetime:
    return datetime.fromisoformat(iso.replace("Z", "+00:00"))


def observation_age_minutes(metar: dict) -> float:
    obs_time = parse_observation_time(metar["parsed"]["time"])
    now = datetime.now(timezone.utc)
    return (now - obs_time).total_seconds() / 60


def fetch_metar(icao: str) -> dict:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    r.raise_for_status()
    return r.json()


# age = observation_age_minutes(data)
# if age > 90:
#     print(f"Warning: observation is {age:.0f} minutes old - station may be offline")

If observation_age_minutes returns more than 90 minutes, the station is likely offline or reporting intermittently. You can still display the data, but flag it as potentially stale.

Caching

METAR and TAF responses change slowly relative to UI refresh rates. Cache METAR for 3–5 minutes and TAF for 15–30 minutes instead of refetching on every render.

EndpointRecommended TTLCache key
GET /weather/metar/{icao}3–5 minutesmetar:{icao}
GET /weather/metar/{icao}?parsed=true3–5 minutesmetar:{icao}:parsed
GET /weather/taf/{icao}15–30 minutestaf:{icao}
GET /weather/taf/{icao}?parsed=true15–30 minutestaf:{icao}:parsed
GET /weather/winds-aloft?bbox=…30–60 minuteswinds:{bbox}:{forecast}:{level}
GET /weather/pireps?bbox=…5–15 minutespirep:{bbox}:{hours}
GET /weather/airsigmet?bbox=…10–15 minutesairsig:{bbox}:{type}
import time
import requests

HEADERS = {
    "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

_cache: dict[str, tuple[dict, float]] = {}


def cached_get(url: str, params: dict, ttl_seconds: int) -> dict | None:
    key = url + str(sorted(params.items()))
    cached = _cache.get(key)
    if cached and time.time() < cached[1]:
        return cached[0]

    r = requests.get(url, headers=HEADERS, params=params, timeout=(10, 15))
    if r.status_code == 404:
        return None
    r.raise_for_status()
    data = r.json()
    _cache[key] = (data, time.time() + ttl_seconds)
    return data


def get_metar(icao: str) -> dict | None:
    return cached_get(f"{BASE}/weather/metar/{icao}", {"parsed": "true"}, ttl_seconds=300)


def get_taf(icao: str) -> dict | None:
    return cached_get(f"{BASE}/weather/taf/{icao}", {"parsed": "true"}, ttl_seconds=1800)

For production, replace the dict-based cache with Redis or Memcached so the cache survives restarts and is shared across instances.

Rate limits

Every response includes rate limit headers:

HeaderValue
X-RateLimit-Requests-LimitYour plan's monthly request quota
X-RateLimit-Requests-RemainingRequests left this month
X-RateLimit-Requests-ResetSeconds until the quota resets

Plan quotas:

PlanRequests/month
Free1,000
Basic5,000
Pro50,000
Ultra200,000
Mega600,000
import requests

HEADERS = {
    "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}


def check_quota() -> dict:
    r = requests.get(
        "https://skylink-api.p.rapidapi.com/weather/metar/KJFK",
        headers=HEADERS,
        timeout=(10, 15),
    )
    r.raise_for_status()

    remaining = int(r.headers.get("X-RateLimit-Requests-Remaining", 0))
    limit      = int(r.headers.get("X-RateLimit-Requests-Limit", 0))
    reset_in   = int(r.headers.get("X-RateLimit-Requests-Reset", 0))

    return {
        "remaining": remaining,
        "limit": limit,
        "reset_in": reset_in,
        "low_quota": remaining < 100,
    }


# quota = check_quota()
# print(f"Quota: {quota['remaining']}/{quota['limit']} remaining, resets in {quota['reset_in']}s")
#
# if quota["low_quota"]:
#     print("Warning: approaching quota limit - reduce polling frequency or upgrade plan")

When you hit 429, the response body is {"message": "Too many requests"} (RapidAPI gateway shape). Back off and retry - see the retry helper on the platform Error Handling page.

Monitoring and logging

Log these on every weather request:

import logging
import time
import requests
from datetime import datetime, timezone

logger = logging.getLogger("weather")
HEADERS = {
    "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"


def fetch_metar_logged(icao: str) -> dict | None:
    start = time.monotonic()
    r = requests.get(f"{BASE}/weather/metar/{icao}", headers=HEADERS, params={"parsed": "true"}, timeout=(10, 15))
    elapsed_ms = (time.monotonic() - start) * 1000

    log_data = {
        "endpoint": "metar",
        "icao": icao,
        "status": r.status_code,
        "elapsed_ms": round(elapsed_ms),
        "quota_remaining": r.headers.get("X-RateLimit-Requests-Remaining"),
    }

    if r.status_code == 200:
        data = r.json()
        if "parsed" in data:
            obs_time = datetime.fromisoformat(data["parsed"]["time"].replace("Z", "+00:00"))
            age_min = (datetime.now(timezone.utc) - obs_time).total_seconds() / 60
            log_data["observation_age_min"] = round(age_min, 1)
            log_data["flight_rules"] = data["parsed"]["flight_rules"]
        logger.info("metar_fetch", extra=log_data)
        return data
    elif r.status_code == 404:
        logger.info("metar_not_found", extra=log_data)
        return None
    else:
        logger.warning("metar_error", extra=log_data)
        r.raise_for_status()

Alert on:

  • observation_age_min > 90 - station is likely offline or updates are delayed
  • status = 429 more than twice in a 5-minute window - your polling rate is too high for your plan
  • elapsed_ms > 5000 repeatedly - upstream latency issue, consider a timeout and fallback
  • quota_remaining < 200 - you're close to your monthly limit

Related: METAR · TAF · Winds Aloft · PIREPs · AIRMET/SIGMET · Error handling · Airport use cases · En-route use cases