On Sunday 25 October, airlines across the northern hemisphere switch to their winter timetables. Summer routes stop, winter routes start and thousands of flights are retimed, all on the same day.

This year's changeover includes several notable launches:

  • Lufthansa starts Frankfurt–Kuala Lumpur on 25 October. It runs five times a week, every day except Tuesdays and Thursdays, as LH704 out of Frankfurt at 21:30 and LH705 back.
  • Air Canada starts Toronto–Tenerife South the same day, using the A321XLR, with Montréal–Tenerife following on 31 October. Air Canada says they're the only nonstop flights between North America and the Canary Islands.
  • Ryanair is calling this its largest winter schedule. New routes are spread across dozens of bases, including four from Kraków to Budapest, Dubrovnik, Rabat and Venice.
  • Delta plans to start Atlanta–Riyadh two days earlier, on 23 October, on an A350-900. It's the first nonstop flight to Riyadh by a US airline. After this week's attacks on Riyadh's airport, there have been reports that Delta is reviewing the launch. When we published, the date hadn't changed.
  • JetBlue has just received final FAA approval for 22 LaGuardia slots that belonged to Spirit, which stopped flying in May: 12 departure slots and 10 arrival slots. Which routes JetBlue puts into them is one of the things to watch this winter.

If you run a travel app, a route-map site, an airport dashboard or an airline-analysis tool, this is the week your data goes stale. This post covers what actually changes at the season switch, and how to detect new and dropped routes automatically instead of reading press releases.

What the winter season is

Airline schedules run in two IATA seasons. The northern winter season starts on the last Sunday of October and runs until the day before the last Sunday of March, so this year it covers 25 October 2026 to 27 March 2027. The summer season covers the rest of the year.

Airlines plan, file and coordinate slots by season, which is why so much changes on one day. For anyone handling flight data, the switch brings a few specific problems:

Routes appear and disappear. Seasonal routes to ski and sun destinations start, and summer leisure routes stop. A route that existed last week may simply not operate until next spring.

Times shift, partly because of clocks. Europe moves its clocks back on the same Sunday, 25 October. The US doesn't change until 1 November. For that one week, New York is four hours behind London instead of five, and transatlantic schedules are retimed to match. Store times in UTC, or this week will catch you out. Zulu time in aviation covers the background.

Frequencies change. A daily summer route may drop to three times a week. If your code assumes a route operates every day, it will see "cancellations" that are just the new timetable.

Announcements aren't operations. A route announced in March can be delayed, cut or brought forward. Delta's Riyadh launch this year shows how quickly plans can change. The only reliable confirmation is the flight actually appearing on the departure board.

Why schedule data, not press releases

You could track new routes by reading airline announcements. It's slow, and it misses most changes, because airlines announce launches and rarely announce cuts. A route that quietly stops is often reported only when someone notices it has gone.

The schedule itself is the better source. If you look at an airport's departures on the same weekday each week and compare them, launches show up as new destinations, cuts show up as missing ones, and retimings show up as changed times. Nothing has to be announced.

SkyLink's schedules endpoint returns an airport's departures or arrivals for a time window, from five days back to one day ahead:

curl "https://skylink-api.p.rapidapi.com/schedules/departures?iata=FRA" \
  -H "X-RapidAPI-Key: YOUR_KEY" \
  -H "X-RapidAPI-Host: skylink-api.p.rapidapi.com"
{
  "iata": "FRA",
  "direction": "departures",
  "airport_code": "FRA",
  "flights": [
    {
      "Time": "21:30",
      "Date": "25 Oct",
      "IATA": "KUL",
      "Destination": "Kuala Lumpur",
      "Flight": "LH 704",
      "Airline": "Lufthansa",
      "Status": "Scheduled"
    }
  ],
  "total_flights": 412,
  "pages_fetched": 5
}

That row is an illustration of the shape, using the published LH704 timing. The real board will have hundreds of rows. Note that the keys are title case: Flight, IATA and Airline, not flight. That catches almost everyone the first time.

The window matters. Because the endpoint looks at most one day ahead, it can't show you 25 October today. It confirms what is operating now and tomorrow. Plan your tracker around that: take a snapshot every day, keep it, and compare snapshots.

The control tower and terminal at New York LaGuardia Airport

Building a route-change tracker

The approach is simple. Once a day, save the full day's departures for each airport you care about. Then compare each day with the same weekday a week earlier.

import json
import requests
from datetime import datetime, timedelta, timezone
from pathlib import Path

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

def board(iata, when):
    """Departures from `when` for roughly the next 12 hours."""
    r = requests.get(f"{BASE}/schedules/departures", headers=H, timeout=30,
                     params={"iata": iata, "ts": int(when.timestamp() * 1000)})
    r.raise_for_status()
    return r.json()["flights"]

def day_of_departures(iata, day):
    """Two 12-hour windows cover the day. Keys are title case: Flight, IATA, Airline."""
    start = datetime(day.year, day.month, day.day, tzinfo=timezone.utc)
    rows = board(iata, start) + board(iata, start + timedelta(hours=12))
    return {(r["Flight"].replace(" ", ""), r["IATA"]): r for r in rows}

def snapshot(iata, day, folder="snapshots"):
    path = Path(folder) / f"{iata}-{day:%Y-%m-%d}.json"
    path.parent.mkdir(parents=True, exist_ok=True)
    flights = day_of_departures(iata, day)
    path.write_text(json.dumps([{"flight": f, "to": to, "airline": r["Airline"]}
                                for (f, to), r in flights.items()]))
    return path

The ts parameter takes a Unix timestamp in milliseconds. The docs recommend it over the date and time strings for scheduled jobs because it avoids time-zone and formatting bugs. The default window is about 12 hours, so two calls 12 hours apart cover a day. Removing spaces from flight numbers matters, because boards show both LH 704 and LH704.

Comparing two snapshots is a set difference:

def load(path):
    return json.loads(Path(path).read_text())

def route_changes(before_path, after_path):
    before, after = load(before_path), load(after_path)
    routes = lambda rows: {(r["airline"], r["to"]) for r in rows}
    b, a = routes(before), routes(after)
    return {"new": sorted(a - b), "gone": sorted(b - a)}

Run that on Frankfurt for Sunday 18 October against Sunday 25 October, and ("Lufthansa", "KUL") should be in the new list. Air Canada to Tenerife should appear the same way at Toronto.

Make the results trustworthy

A naive diff produces a lot of false alarms. Four rules remove most of them:

Compare the same weekday. Lufthansa's Kuala Lumpur flight doesn't operate on Tuesdays or Thursdays. Compare a Tuesday with a Monday and you'll see it "cut" and "relaunched" every week.

Require two weeks before calling a route gone. A single missing day might be one cancelled flight. A route that is missing on the same weekday two weeks running has probably been cut, or paused for the season.

Watch out for codeshares. Departure boards often list the same flight under several flight numbers, one for each airline selling it. A new codeshare agreement can look like a dozen new routes. Grouping by destination and departure time, rather than flight number, collapses them. Codeshare flights explained covers the details.

Keep the raw snapshots. Store each day's file. When someone asks when a route actually started, you'll have the answer.

Checking against the route database

Once you've detected a new route, you can check whether SkyLink's route database already knows about it, and get the distance and typical block time for your route page:

def route_info(dep, arr):
    r = requests.get(f"{BASE}/routes/pairs", headers=H, timeout=15,
                     params={"departure": dep, "arrival": arr, "limit": 5})
    r.raise_for_status()
    return r.json()["routes"]

If the pair isn't there yet, that's itself a strong sign the route is new. The route database describes the network in general, so for "is it flying this week" the schedule snapshots are the source of truth.

Tenerife South Airport, with Mount Teide behind

What it costs to run

Each airport needs two requests a day, so tracking 50 airports costs about 3,000 requests a month. That fits within Basic's 5,000. Tracking 200 airports is about 12,000 a month, which needs Pro's 20,000. Both directions, departures and arrivals, double those numbers. That's only worth doing if you care about the inbound side.

Watch the per-second rate limit when you run the daily job. On Basic it's two requests a second, so fetch airports one after another rather than all at once.

Using the results

Route launch pages. "New: Frankfurt to Kuala Lumpur" pages built the day the first flight appears on the board, rather than weeks later.

Network change reports. A weekly list of what each airline added and dropped at the airports you follow is useful to travel trade media, airports and analysts.

Keeping travel apps accurate. If a user saved a route for later and it's been dropped for the winter, tell them before they search for it in January.

Watching JetBlue at LaGuardia. Run the tracker on LGA and you'll see the new routes appear as JetBlue starts using the ex-Spirit slots, without waiting for an announcement.

For single flights rather than whole airports, flight status gives the gate, terminal and current times. Our flight schedules API guide covers the schedule endpoints in more depth, and the Flight Operations page shows how they fit together.

The tracker is worth setting up before the 25th, so you have a week of summer snapshots to compare against. You can try it on the free trial, which is for personal, non-commercial use. For a product, see pricing.