Caching & monitoring
METAR/TAF 404 fallbacks: Error handling. Platform HTTP errors and retries: Error Handling.
Data freshness
| Data type | Update schedule | Notes |
|---|---|---|
| METAR | Every 30 min (major airports), every 60 min (smaller stations) | SPECI reports go out immediately on significant condition changes |
| TAF | 4 times per day - 00Z, 06Z, 12Z, 18Z | Valid for 24 or 30 hours; a TAF is still current between issuances |
| Winds Aloft (FB) | Fixed FAA product schedule | US only; valid_time on response marks forecast validity |
| PIREPs | Continuous filing by pilots | Ephemeral - use a short look-back (hours param) |
| AIRMET/SIGMET | Issued and updated on FAA schedule | Expire 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.
| Endpoint | Recommended TTL | Cache key |
|---|---|---|
GET /weather/metar/{icao} | 3–5 minutes | metar:{icao} |
GET /weather/metar/{icao}?parsed=true | 3–5 minutes | metar:{icao}:parsed |
GET /weather/taf/{icao} | 15–30 minutes | taf:{icao} |
GET /weather/taf/{icao}?parsed=true | 15–30 minutes | taf:{icao}:parsed |
GET /weather/winds-aloft?bbox=… | 30–60 minutes | winds:{bbox}:{forecast}:{level} |
GET /weather/pireps?bbox=… | 5–15 minutes | pirep:{bbox}:{hours} |
GET /weather/airsigmet?bbox=… | 10–15 minutes | airsig:{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:
| Header | Value |
|---|---|
X-RateLimit-Requests-Limit | Your plan's monthly request quota |
X-RateLimit-Requests-Remaining | Requests left this month |
X-RateLimit-Requests-Reset | Seconds until the quota resets |
Plan quotas:
| Plan | Requests/month |
|---|---|
| Free | 1,000 |
| Basic | 5,000 |
| Pro | 50,000 |
| Ultra | 200,000 |
| Mega | 600,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 delayedstatus = 429more than twice in a 5-minute window - your polling rate is too high for your planelapsed_ms > 5000repeatedly - upstream latency issue, consider a timeout and fallbackquota_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