How to Measure Airport Congestion from Taxi-Out Times

On this page

Problem statement

A departure delay measures what happened at the gate. Once the aircraft pushes back, it can still sit in a queue for the runway for half an hour, and that time does not show up as a departure delay at all β€” the flight left "on time". The BTS on-time file records it separately as TaxiOut, minutes from gate departure to wheels off, for every flight. It is the most direct measure of airfield congestion in the public data.

This page ranks 2025 taxi-out times by airport, separates the queue from each airport's ordinary taxi distance, and looks at when the queues form: by hour, by season, and in relation to gate delays.

Quick answer

Per airport, take the median taxi-out time, its 90th percentile, and its 10th percentile as the "unimpeded" time β€” how long the taxi takes when nobody is in the way. The difference is the queue:

dep = flights[(flights.Cancelled == 0) & flights.TaxiOut.notna()]
by = dep.groupby("Origin").TaxiOut.agg(departures="size", median="median", p90=lambda t: t.quantile(0.9),
                                       unimpeded=lambda t: t.quantile(0.1))
big = by[by.departures >= 20_000]
big = big.assign(excess=big["median"] - big.unimpeded)
print(big.sort_values("median", ascending=False).head(6).round(1).to_string())
print(big.sort_values("median").head(3).round(1).to_string())
        departures  median   p90  unimpeded  excess
Origin                                             
JFK         102644    24.0  47.0       15.0     9.0
ORD         321372    23.0  41.0       14.0     9.0
SEA         163487    21.0  33.0       14.0     7.0
EWR         117258    21.0  42.0       14.0     7.0
LGA         131602    21.0  42.0       14.0     7.0
SAN          95816    20.0  33.0       13.0     7.0
        departures  median   p90  unimpeded  excess
Origin                                             
OGG          25284    10.0  15.0        7.0     3.0
DAL          70315    11.0  19.0        8.0     3.0
OAK          34368    11.0  17.0        8.0     3.0

JFK has the longest median taxi-out of the large airports, 24 minutes, and O'Hare 23; one JFK departure in ten taxis for 47 minutes or more. At Kahului, Dallas Love Field and Oakland the median is 10–11 minutes. Subtracting the unimpeded time separates layout from congestion: at JFK and O'Hare, 9 minutes of the median is waiting, at the shortest airports 3.

Dot plot of taxi-out times for the 18 large airports with the longest medians: each row shows the 10th percentile, the median and the 90th percentile. JFK leads with a median of 24 minutes and a 90th percentile of 47; O'Hare follows at 23. Dot plot of taxi-out times for the 18 large airports with the longest medians: each row shows the 10th percentile, the median and the 90th percentile. JFK leads with a median of 24 minutes and a 90th percentile of 47; O'Hare follows at 23.
Taxi-out times by airport, 2025. The distance between the green and dark dots is the typical queue.

From gate to wheels off

1. What taxi-out measures

print(dep.TaxiOut.describe(percentiles=[0.5, 0.9, 0.99, 0.999]).round(0).to_string())
print(f"departures taxiing over two hours: {(dep.TaxiOut > 120).sum():,}")
count    6898743.0
mean          19.0
std           11.0
min            1.0
50%           16.0
90%           30.0
99%           61.0
99.9%        110.0
max         1274.0
departures taxiing over two hours: 4,363

The median departure taxis for 16 minutes and one in a hundred for an hour or more. 4,363 taxied for over two hours β€” long ground holds, de-icing queues and tarmac delays, which have their own federal reporting rules.

2. Excess over the airport's own unimpeded time

dep = dep.assign(excess=dep.TaxiOut - dep.Origin.map(by.unimpeded))
total = dep[dep.Origin.isin(big.index)].groupby("Origin").excess.sum() / 60
print("hours of taxi time above each airport's 10th percentile, 2025:")
print(total.sort_values(ascending=False).head(6).round(0).astype(int).to_string())
hours of taxi time above each airport's 10th percentile, 2025:
Origin
ORD    64289
DEN    41557
DFW    39003
ATL    31041
LGA    26017
CLT    25949

Summed over the year, the time above each airport's own unimpeded taxi is large: O'Hare's departures spent 64,289 hours in queues, Denver's 41,557, DFW's 39,003. Ranking by total rather than per flight rewards size as much as congestion, so both views are worth keeping.

3. Queues by hour

hour = dep.CRSDepTime.str[:2].astype(int).clip(upper=23)
q = {}
for code in ["JFK", "ORD", "ATL", "DEN"]:
    d = dep[dep.Origin == code]
    h = hour[d.index]
    q[code] = pd.DataFrame({"departures per day": d.groupby(h).size() / 365, "median taxi": d.groupby(h).TaxiOut.median()})
    print(code, f"correlation of hourly departures and median taxi-out: {q[code].corr().iloc[0, 1]:.2f}")
print(q["JFK"].loc[[6, 8, 12, 15, 17, 19, 22]].round(1).to_string())
JFK correlation of hourly departures and median taxi-out: 0.44
ORD correlation of hourly departures and median taxi-out: 0.73
ATL correlation of hourly departures and median taxi-out: 0.19
DEN correlation of hourly departures and median taxi-out: 0.84
            departures per day  median taxi
CRSDepTime                                 
6                         18.8         19.0
8                         24.9         26.0
12                        12.3         19.0
15                        20.0         25.0
17                        19.2         29.0
19                        17.0         33.0
22                         6.0         25.0

Where the queue tracks the schedule, the correlation between departures per hour and median taxi-out is high: 0.84 at Denver, 0.73 at O'Hare. At JFK it is weaker (0.44) because the evening is worse than the schedule alone predicts: 19 departures an hour at 7 p.m. taxi a median 33 minutes, while 25 an hour at 8 a.m. taxi 26. The likeliest reason is JFK's evening international departures, which share the runways but are not in this domestic file. Atlanta's correlation is only 0.19: its queues barely follow its hourly schedule.

Four panels for JFK, O'Hare, Denver and Atlanta with bars of departures per hour and a line of median taxi-out time. At Denver and O'Hare the line follows the bars; at JFK it rises in the evening beyond what the bars suggest; at Atlanta it stays nearly flat. Four panels for JFK, O'Hare, Denver and Atlanta with bars of departures per hour and a line of median taxi-out time. At Denver and O'Hare the line follows the bars; at JFK it rises in the evening beyond what the bars suggest; at Atlanta it stays nearly flat.
Departures per hour (bars) and median taxi-out (line) by scheduled hour, 2025.

4. Summer queues, winter de-icing

m = dep.FlightDate.dt.month
season = dep[m.isin([1, 7])].groupby(["Origin", m[m.isin([1, 7])]]).TaxiOut.median().unstack()
season.columns = ["January", "July"]
season = season.loc[big.index].assign(diff=lambda d: d.January - d.July)
print(season.sort_values("diff").iloc[[0, 1, 2, -3, -2, -1]].to_string())
        January  July  diff
Origin                     
JFK        21.0  27.0  -6.0
ORD        20.0  24.0  -4.0
SFO        17.0  20.0  -3.0
PIT        18.0  14.0   4.0
BUF        18.0  12.0   6.0
CLE        20.0  13.0   7.0

The expectation that winter is worse holds only at some airports. At Pittsburgh, Buffalo and Cleveland the January median is 4–7 minutes longer than July's: aircraft de-ice on the way to the runway. At JFK and O'Hare it is the other way round, by 6 and 4 minutes: summer schedules are denser and thunderstorms close departure routes, which keeps aircraft queued on the taxiway. The busiest airports' worst season for queues is summer.

Taxi time and punctuality

The data used on this page

The Bureau of Transportation Statistics Reporting Carrier On-Time Performance files for 2025, which carry taxi-out (gate departure to wheels off) and taxi-in times for every flight, and airport coordinates from OurAirports in a copy saved on 15 October 2026.

from pathlib import Path
import zipfile

import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
BTS = "https://transtats.bts.gov/PREZIP/On_Time_Reporting_Carrier_On_Time_Performance_1987_present_2025_{}.zip"
COLS = ["FlightDate", "Reporting_Airline", "Tail_Number", "Flight_Number_Reporting_Airline", "Origin", "Dest",
        "CRSDepTime", "DepTime", "DepDelay", "TaxiOut", "WheelsOff", "WheelsOn", "TaxiIn", "CRSArrTime", "ArrTime",
        "ArrDelay", "Cancelled", "CancellationCode", "Diverted", "CRSElapsedTime", "ActualElapsedTime", "AirTime",
        "Distance", "CarrierDelay", "WeatherDelay", "NASDelay", "SecurityDelay", "LateAircraftDelay"]
TEXT = {c: str for c in ["CRSDepTime", "DepTime", "WheelsOff", "WheelsOn", "CRSArrTime", "ArrTime", "CancellationCode",
                         "Tail_Number"]}


def read_month(m):
    f = Path(f"data/On_Time_Reporting_Carrier_On_Time_Performance_1987_present_2025_{m}.zip")
    if not f.exists():
        f.write_bytes(requests.get(BTS.format(m), timeout=900).content)
    with zipfile.ZipFile(f) as z:
        name = next(n for n in z.namelist() if n.endswith(".csv"))
        return pd.read_csv(z.open(name), usecols=COLS, dtype=TEXT, parse_dates=["FlightDate"], low_memory=False)


flights = pd.concat([read_month(m) for m in range(1, 13)], ignore_index=True)

# OurAirports is edited daily; this is the copy of 15 October 2026
# (live: https://davidmegginson.github.io/ourairports-data/airports.csv).
AIRPORTS = "ourairports-airports-2026-10-15.csv"
if not Path(f"data/{AIRPORTS}").exists():
    Path(f"data/{AIRPORTS}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{AIRPORTS}", timeout=300).content)
ap = pd.read_csv(f"data/{AIRPORTS}", usecols=["ident", "iata_code", "name", "latitude_deg", "longitude_deg"])
by_iata = ap.dropna(subset=["iata_code"]).drop_duplicates("iata_code").set_index("iata_code")
by_ident = ap.set_index("ident")
codes = pd.Index(sorted(set(flights.Origin) | set(flights.Dest)))
KEEP = ["name", "latitude_deg", "longitude_deg"]
airports = by_iata.reindex(codes)[KEEP]
gap = airports.latitude_deg.isna().values
airports.loc[gap, KEEP] = by_ident.reindex("K" + codes[gap])[KEEP].values      # fall back to the US ident, K + code
airports = airports.rename(columns={"latitude_deg": "lat", "longitude_deg": "lon"})
print(f"{len(flights):,} flights, {len(codes)} airports, {airports.lat.isna().sum()} without coordinates")
7,001,619 flights, 352 airports, 0 without coordinates

Example 1: taxi-out against the departure delay

late_gate = dep.DepDelay >= 15
print(f"median taxi-out when the flight left the gate on time: {dep.TaxiOut[~late_gate].median():.0f} min; "
      f"when it left 15+ min late: {dep.TaxiOut[late_gate].median():.0f} min")
on_time_gate = dep[(dep.DepDelay <= 0) & dep.ArrDelay.notna()]
print(f"flights that pushed back on time but arrived 15+ min late: {(on_time_gate.ArrDelay >= 15).mean():.1%}; "
      f"their median taxi-out {on_time_gate.TaxiOut[on_time_gate.ArrDelay >= 15].median():.0f} min")
median taxi-out when the flight left the gate on time: 16 min; when it left 15+ min late: 16 min
flights that pushed back on time but arrived 15+ min late: 4.1%; their median taxi-out 35 min

Flights that left the gate late do not taxi longer β€” the median is 16 minutes either way. But taxiing is where an on-time departure can still become a late arrival: 4.1% of flights that pushed back on time arrived 15 or more minutes late, and their median taxi-out was 35 minutes, more than twice the usual.

Example 2: taxi-in at the arrival end

tin = flights[(flights.Cancelled == 0) & flights.TaxiIn.notna()].groupby("Dest").TaxiIn.agg(arrivals="size", median="median")
both = big.join(tin["median"].rename("taxi-in median"))
print(f"correlation of median taxi-out and taxi-in at large airports: {both['median'].corr(both['taxi-in median']):.2f}")
print(both.sort_values("taxi-in median", ascending=False).head(4)[["median", "taxi-in median"]].to_string())
correlation of median taxi-out and taxi-in at large airports: 0.64
        median  taxi-in median
Origin                        
ORD       23.0            16.0
CLT       19.0            12.0
DFW       17.0            12.0
SEA       21.0            10.0

Taxi-in times are shorter but correlate with taxi-out across large airports (0.64): the same sprawling airfields are slow in both directions. O'Hare is slowest at both ends, with a median 16 minutes from touchdown to gate.

Scatter of median taxi-out in January against July for large airports, with a dashed equality line. Cleveland, Buffalo and Pittsburgh sit above the line, longer in January; JFK, O'Hare and San Francisco sit below it, longer in July. Scatter of median taxi-out in January against July for large airports, with a dashed equality line. Cleveland, Buffalo and Pittsburgh sit above the line, longer in January; JFK, O'Hare and San Francisco sit below it, longer in July.
De-icing lengthens winter taxi times at some northern airports; at the busiest airports summer queues are worse.

The queue, measured by subtraction

A taxi-out time is the sum of two things: the distance from the gate to the runway, which is fixed by the airfield, and the time spent waiting, which is congestion. The file does not separate them, but the distribution does. The fastest taxis at an airport happen when nobody is in the way, so a low percentile approximates the unimpeded time, and everything above it is queue. The FAA's own unimpeded-taxi estimates are made in a more careful version of the same way.

Seen as a queue, taxi time responds to what queues respond to: the number of departures per hour, the number of usable runways, and anything that reduces the departure rate β€” thunderstorms on departure routes in summer, de-icing in winter. The airport-level medians on this page mix all of these; the hour-by-hour and month-by-month views separate them enough to show that the busiest airports are most congested in summer evenings, not in winter.

Reading taxi times

  • Taxi-out starts at gate departure. A flight held at the gate shows the hold as departure delay, not taxi time.
  • Use the airport's own baseline. Raw medians mix layout with congestion.
  • Long holds are rare but real. 4,363 departures in 2025 taxied for more than two hours.
  • The file covers domestic flights only. International departures share the runways and the queue.
  • Totals reward size, rates reward congestion. Report both.

FAQ

Which US airport has the longest taxi-out times?

JFK, with a median of 24 minutes for domestic departures in 2025 and a 90th percentile of 47, followed by Chicago O'Hare at 23.

Is taxi time longer in winter?

At airports that de-ice routinely, such as Buffalo and Cleveland, yes. At the busiest airports, summer is worse: JFK's July median was 6 minutes longer than January's.