Guides · September 21, 2026 · 6 min read

Missed connection alerts from two subscriptions

A tight connection is not a property of either flight; it lives in the gap between them. How to compute that gap from two watched flights, choose a threshold per airport, handle terminals, early departures, cancellations and diversions, and keep the alert from firing every time an estimate moves by a minute.

"Your connection is at risk" is the most valuable sentence an itinerary app can send, and the easiest to get wrong. Send it late and it is useless. Send it on every wobble of an estimate and the user stops reading. And no flight status API can send it for you, because the fact does not belong to a flight. It belongs to two flights and one passenger.

This guide builds it from two subscriptions and about eighty lines of state.

The quantity

For a passenger arriving on flight A and leaving on flight B from the same airport:

layover = B.departure.bestTimeUtc − A.arrival.bestTimeUtc

bestTimeUtc is the best time known right now: actual if it has happened, else estimated, else scheduled. Both are gate times, which is what you want. A passenger is not connecting until the door opens, and is not on board after the door closes.

Compare the layover with a threshold you choose. Call the difference the margin. Positive margin, the connection is fine. Negative, it is not. The interesting band is the one in between.

FlightByte reports flights, not journeys

It does not know these two flights are one person's trip, and it does not publish minimum connection times. You bring the pairing and the threshold; it keeps both halves current. Connection, layover and MCT has the vocabulary.

Finding the pairs

A connection exists in your data when two flights on one trip satisfy all of these:

  • The arrival airport of the first is the departure airport of the second. Compare arrival.airport.iata with departure.airport.iata.
  • The second is scheduled to leave after the first is scheduled to arrive, by less than a day.
  • They belong to the same traveller.

Some cities need care. A passenger landing at one London airport and leaving from another has a connection your airport comparison will miss, and no status feed will help with the coach between them. Decide whether you support that, and if you do, treat it as a pairing with a very large threshold.

Store the pair:

create table connections (
  id            text primary key,
  trip_id       text not null,
  inbound       text not null references watched_flights(id),
  outbound      text not null references watched_flights(id),
  threshold_min int  not null,
  state         text not null default 'ok',   -- ok | tight | at_risk | broken
  announced_min int                           -- layover at the last alert
);

Choosing a threshold

Published minimum connection times vary from twenty-five minutes at a small domestic airport to two hours for an international transfer between terminals at a large one. They are not in the snapshot. A workable default table is short:

SituationThreshold
Same terminal, both domestic or both within one border-free area45 min
Same terminal, one leg international75 min
Different terminals90 min
Separate tickets, bags to recheck120 min

Read the terminals from arrival.terminal on the inbound and departure.terminal on the outbound. They are often absent until the day; start with the same-terminal assumption and re-evaluate when either appears. Override per airport as you learn which ones are slow. Whatever you choose, show the user the layover in minutes, not just your verdict, so that a traveller who knows the airport can disagree.

Subscribing for it

Both flights are probably already watched for their own sake. Make sure these change types are in each subscription's events:

FlightChange types that move the margin
Inboundarrival.estimated.changed, arrival.actual.changed, delay.increased, delay.decreased, arrival.terminal.changed, flight.cancelled, flight.diverted, flight.reinstated
Outbounddeparture.estimated.changed, departure.scheduled.changed, departure.terminal.changed, departure.gate.changed, flight.cancelled, flight.reinstated, flight.departed

Keep delayThresholdMinutes low on flights that are part of a connection, five rather than fifteen. A twelve-minute slip that nobody would push to a passenger on its own can be the whole margin of a fifty-minute connection.

The evaluator

Run it whenever a delivery touches either flight of a pair. It reads the two stored snapshots, so it does not matter which flight the event was about or in what order events arrived.

type State = "ok" | "tight" | "at_risk" | "broken";

const TIGHT_BAND = 20;   // minutes above the threshold that still deserve a heads-up
const STEP = 10;         // re-announce only after this much further movement

export function evaluate(conn: Connection, inbound: Snapshot, outbound: Snapshot) {
  if (inbound.status.cancelled || inbound.status.diverted) return verdict(conn, "broken", null, "inbound");
  if (outbound.status.cancelled) return verdict(conn, "broken", null, "outbound");

  const arrive = Date.parse(inbound.arrival.bestTimeUtc);
  const depart = Date.parse(outbound.departure.bestTimeUtc);
  const layover = Math.round((depart - arrive) / 60_000);
  const margin = layover - thresholdFor(conn, inbound, outbound);

  const state: State = margin < 0 ? "at_risk" : margin < TIGHT_BAND ? "tight" : "ok";
  return verdict(conn, state, layover);
}

function verdict(conn: Connection, state: State, layover: number | null, cause?: string) {
  const worse = rank(state) > rank(conn.state);
  const better = rank(state) < rank(conn.state);
  const moved = layover !== null && conn.announcedMin !== null && Math.abs(layover - conn.announcedMin) >= STEP;

  // Speak when it gets worse, when it recovers, or when a bad situation moves a lot.
  const speak = worse || (better && conn.state !== "ok") || (state !== "ok" && moved);
  return { state, layover, cause, speak };
}

Three rules are doing the work, and they are the same three that keep a delay notifier quiet:

  • A band, not a line. tight exists so the first message is "this is getting close", sent while the user can still do something, rather than "you have missed it".
  • A step. Inside a bad state, say nothing until the layover has moved another ten minutes. Estimates drift by one or two minutes all afternoon.
  • Recovery is news. When an announced risk goes away, say so, once. People rebook on the strength of your first message.

The cases that bite

  • The outbound leaves early. It happens, particularly on short sectors with a slot to catch. The formula handles it, because departure.bestTimeUtc moves earlier. What breaks is an implementation that only listens to the inbound. Listen to both.
  • The inbound has landed. Once arrival.times.actualGate exists, the arrival side is a fact. From then on only the outbound can change the margin, and the alert should switch from "at risk" to directions: gate, terminal, minutes.
  • The outbound has departed. flight.departed on the outbound while the inbound is not yet arrived is the end of the story. Mark it broken, stop evaluating, and change the screen from a warning to help.
  • Diverted inbound. The arrival airport is no longer the connection airport. status.diverted is true and diverted names where the aircraft went. No arithmetic applies; hand the user to a human or a rebooking flow.
  • Reinstated. Cancellations are sometimes reversed. flight.reinstated should put the pair back through the evaluator rather than leaving it broken for ever.
  • Terminal appears late. A terminal published two hours before departure can move the threshold from 45 to 90 and flip a comfortable connection to tight with no change in any time. Re-evaluate on arrival.terminal.changed and departure.terminal.changed as well as on times.
  • Three legs. Evaluate each adjacent pair independently. A late first leg propagates by itself, because the second leg's risk is computed from real estimates, not from your guess about the first.

What to say

State changeMessage
ok to tightYour connection in Istanbul is down to 58 min. TK1848 now lands 13:58; TK1 leaves 14:56 from the same terminal.
tight to at riskConnection at risk: 32 min in Istanbul. TK1 is still scheduled for 14:56.
at risk, movedNow 18 min. Speak to the crew before landing; they can call ahead.
at risk to okGood news: TK1 is delayed to 15:40. You have 1 h 16 min.
broken, inbound cancelledTK1848 is cancelled, so your connection to TK1 will not work. Tap for options.

Times in those sentences come from times.*.local on each flight, which is the clock at that airport. Never build them from bestTimeUtc and a guess at the zone.

Testing it

Unit-test the evaluator with pairs of snapshots; it is a pure function. For the wiring, send signed fixtures to your local receiver with the script in the example receivers repo: an arrival.estimated.changed that eats twenty minutes, a second that eats two more and must be silent, then a departure.estimated.changed on the outbound that gives thirty back and must announce recovery.

What it costs

Nothing beyond the two flights. A connection is not a billable thing; it is arithmetic over two subscriptions you were already paying for, each counted once however many travellers and connections share it.

More guides

Get started

Watch your first flight tonight.

Nine dollars, a hundred flights, every event, signed. The receiver above is the whole integration.

A free key in a minute, no card. On a paid plan? Request a key