"Your flight is delayed" is one of the most valuable push notifications a travel app can send — and one of the easiest to get wrong. Poll too aggressively and you burn your API quota and your rate limit; poll too slowly and the alert arrives after the passenger is already standing at the gate. This post walks through the architecture of a flight delay notification system that most teams converge on: a polling loop, a cache that respects data freshness, a change-detection step, and an alert dispatcher — and how each piece maps onto real flight-data endpoints.
The core loop
Flight status has no free real-time push feed the way, say, a chat API does — you find out a flight is delayed by asking. So every delay-notification system is fundamentally a polling loop over the flights people care about:
for each tracked flight:
fetch current status
compare against last known status
if a meaningful field changed:
update the store
enqueue notifications for subscribers
sleep until next pollThe whole design problem is making that loop cheap, timely, and free of false alarms. That comes down to three decisions: how often you poll, what you cache, and what counts as a "meaningful" change.

Polling: track the right thing at the right rate
Start by getting the status of a single flight. SkyLink API's flight status endpoint takes a flight number and returns departure and arrival times, terminal, gate, and status:
curl "https://skylink-api.p.rapidapi.com/flight_status/BA123" \
-H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
-H "X-RapidAPI-Host: skylink-api.p.rapidapi.com"{
"flight_number": "BA 123",
"airline": "British Airways",
"status": "En Route",
"departure": {
"airport": "EGLL",
"scheduled_time": "10:30",
"actual_time": "10:35",
"terminal": "5",
"gate": "A12"
},
"arrival": {
"airport": "KJFK",
"scheduled_time": "14:45",
"estimated_time": "14:50",
"terminal": "7",
"gate": "B15",
"baggage": ""
}
}Two things shape your polling strategy immediately:
Don't poll at a fixed rate — poll on a curve. A flight departing in 36 hours changes almost never; a flight due to push back in 40 minutes can change gate, time, and status repeatedly. Scale the interval to time-until-departure:
| Time until departure | Poll interval |
|---|---|
| > 24 h | every 3–6 h |
| 6–24 h | every 1 h |
| 2–6 h | every 15–20 min |
| < 2 h to landing | every 3–5 min |
| Landed / cancelled | stop polling |
Empty strings mean "unknown," not "error." Gate, terminal, baggage, and check-in fields are frequently "" until the airline publishes them — note the empty baggage above. Treat an empty string as not yet known and never fire an alert on the "" → "B15" transition as if it were a change the user asked about; it's an initial publish, not a delay.
If you're tracking whole airports rather than individual itineraries — an operations dashboard, a lounge display — use the schedules endpoint to pull all departures or arrivals in one call instead of looping over flight numbers. And on US airspace, layer in the FAA delays endpoint, which surfaces Ground Delay Programs and Ground Stops before they show up as per-flight time changes — an early-warning signal that a whole airport is about to slip.
Caching: fast reads without stale alerts
You cache for two independent reasons, and it's worth keeping them separate in your head:
- Read cache — so your app's UI and repeated subscriber lookups don't each hit the API. This is a short TTL keyed by flight number.
- Last-known-state store — the previous status you compare against to detect a change. This is durable, not a cache, and it's the heart of the whole system.
The trap is caching so long that you miss the very change you exist to detect. Match TTL to how fast the underlying data moves. Per-flight status can be cached a few minutes; FAA delay programs should not be cached longer than 2–5 minutes because Ground Stops change with little notice. A reasonable pattern:
const TTL_SECONDS = 180; // 3 min for in-window flights
const HEADERS = {
"X-RapidAPI-Key": process.env.RAPIDAPI_KEY,
"X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
};
async function fetchFlightStatus(flightNumber) {
const res = await fetch(
`https://skylink-api.p.rapidapi.com/flight_status/${flightNumber}`,
{ headers: HEADERS }
);
return res.json();
}
async function getStatus(flightNumber) {
const cached = await cache.get(flightNumber);
if (cached && Date.now() - cached.fetchedAt < TTL_SECONDS * 1000) {
return cached.data; // serve without an API call
}
const data = await fetchFlightStatus(flightNumber);
await cache.set(flightNumber, { data, fetchedAt: Date.now() });
return data;
}Cache reads, but drive change detection off every fresh fetch — don't let a cache hit silently skip the comparison step, or you'll cache your way past a delay.

Change detection: only alert on what matters
This is where naive systems generate angry users. If you diff the entire response, you'll alert on gate assignments appearing, estimated times ticking by a minute, and every "" filling in. Define a small set of meaningful transitions and only notify on those:
function meaningfulChanges(prev, next) {
const events = [];
// Status change (scheduled -> delayed, -> cancelled, -> boarding)
if (prev.status !== next.status) {
events.push({ type: "status", from: prev.status, to: next.status });
}
// Departure slipped by more than a threshold
const slip = minutesBetween(prev.departure.scheduled_time,
next.departure.actual_time || next.departure.scheduled_time);
if (slip >= 15) {
events.push({ type: "delay", minutes: slip });
}
// Gate change — but only once a gate was actually published
if (prev.departure.gate && next.departure.gate &&
prev.departure.gate !== next.departure.gate) {
events.push({ type: "gate", from: prev.departure.gate, to: next.departure.gate });
}
return events;
}Three rules keep it sane:
- Threshold your delays. A 2-minute slip isn't news; a 15-minute (or 30-minute) slip is. Pick a threshold and debounce around it so a time oscillating between +12 and +18 minutes doesn't spam.
- Guard the "first publish" case. A gate or baggage belt going from empty to populated is information appearing, not changing. Require both the old and new value to be non-empty before you treat it as a change.
- Deduplicate. Store the last event you sent per flight per subscriber and suppress a repeat of the same delay within a cooldown window.
Alerts: decouple detection from delivery
Don't send push notifications from inside the polling loop. The moment detection and delivery share a thread, a slow APNs/FCM call stalls your polling and a delivery failure risks dropping the state update. Split them with a queue:
poller ──> writes state + enqueues events ──> [ queue ] ──> notifier workersThe poller's only job is to fetch, compare, persist the new state, and enqueue an event. Separate notifier workers pull events and handle the delivery mechanics — fan-out to every subscriber of that flight, per-channel formatting (push, SMS, email, webhook), retries, and rate limiting per user. This makes delivery failures retryable without re-polling, and it lets you scale notification throughput independently of poll frequency.
Putting it together
The full system is small once the pieces are named: a scheduler that wakes tracked flights on a time-until-departure curve, a fetcher that reads through a short-TTL cache, a comparator that diffs against the last-known-state store for meaningful transitions only, and a queue + workers that deliver. Add FAA delay polling for US airports as an early-warning overlay, and you have a notification system that's timely without being wasteful.
For the delay-prediction side — estimating a delay before the airline publishes it — look at the ML predictions endpoint, which complements live status with a modeled estimate. And if you're building the surrounding stack, the flight status, schedules, and FAA delays endpoints all live under the same flight operations feature set.
SkyLink API gives you a free tier of 1,000 requests/month to prototype your polling loop against, with paid plans starting at $18.59/mo once you're tracking real traffic. It's available through the free trial — sign up, grab a key, and start turning flight status into timely delay alerts.
