How long does it take to go through an airport?

How long does it take to go through an airport?
If you've never missed a flight, you're probably spending too much time in airports. - George Stigler

When you're planning a flight, deciding when to arrive is a delicate balancing act. If you show up too soon, you might spend precious hours waiting around, but if you're too late, you could miss your flight. If you're risk neutral, then you should arrive at each flight later and later until you miss a flight and then stop around there. If you're risk averse, like me, you should arrive early.

But how early is too early?

To help figure that out, I've logged the time it took me to clear each point of an airport departure and arrival. When we plot the time taken, we can see that there's a wide range of outcomes. Some flights are a breeze, others require hours spent lining up. Here's what I found, some pretty graphs and the R code I used to create them.

Check-in takes the most time at departure, immigration the most at arrivals

There's five main areas you need to pass through when you catch a flight: check-in, emigration, security at departure, followed by immigration and customs at arrival. Many folks also collect their bags at arrival, but all my flights were carry-on only. And then there's also walking between areas and waiting around.

From this we see that median time to checkin was 11 minutes, half the time checkin took less than 11 minutes, while half took more. The median time to clear emigration was 2 minutes thanks for Changi's automatic passport scanners. The median time to clear security was 6 minutes, 5 for immigration and 0 for customs (because over half of flights had no customs).

However, each area is skewed by a few, lengthy periods of time. For instance, I once spent over an hour lining up to checkin for a flight from Singapore to Fiji. A flight from Dhaka to Singapore took 33 minutes to clear emigration. I once spent half an hour in security from Kuala Lumpur to Singapore.  

We also need to factor in that there's some walking between areas, even if my data isn't a useful guide for how long that should take. Suppose we spend five minutes in-between each area, plus five minutes boarding at the gate, that's 20-30 minutes per departure.

It takes just over half an hour to go from touchdown to terminal on average

Unlike departure, arrivals has no endogenous waiting. Your plane lands, and you want to reach the terminal as soon as you can. The fastest I've managed to go from touchdown to terminal is 8 minutes at a regional airport. The longest was an hour. The median was just over half an hour.

If sitting up the back makes a difference, it's minor

I personally prefer to sit in an window seat because at heart I'm still a small child who presses their face against the glass and wonders at the world outside. The catch is that purchasing a window seat costs extra, but it costs less if you sit up the back. However, this may mean that it takes longer to leave the plane.

Thankfully, we see no strong pattern between how far back you sit and how long it takes to leave the plane. If you sat in the very first row, then our models estimates that you'd take, on average, 7 minutes between the seatbelts sign turning off and leaving through the gate. Whereas if you sat way back in row 40, you'd take 10 minutes on average.

No doubt where you sit makes some difference, but the effect of sitting further back is swamped by other factors like busyness of the plane and number of aisles.

Conclusion

Across all my flights, the median total time to clear each area of departures was just shy of half an hour. If we add in walking from area to area it comes to one hour. However, the median masks a lot of variation. So you could comfortably arrive at the airport 1.5 to 2 hours beforehand, depending on the airport and whether you're flying at a peak timeslot.

When it comes to alighting, I don't think sitting up the back makes a huge difference. If you want to enjoy a window seat, get a cheap one up the back. It won't keep you waiting much longer.

Happy travels

Appendix: R Code

Setup

# Use pacman
if (!require("pacman")) {install.packages("pacman")}

# Load packages
pacman::p_load(
  install = F,
    char = c(
    "tidyverse",
    "here",
    "broom",
    "lubridate",
    "naniar",
    "janitor"))

Read

df_flights <-
  read_csv(
    here::here("Flight Log.csv"), 
    na = naniar::common_na_strings) %>% 
  janitor::clean_names() 

df_flights <- 
  df_flights %>% 
  select(-c(10, 19, 32)) %>% 
  mutate(across(terminal_arrive:terminal_outside, ~ lubridate::hms(.x)))

Clean Airport Areas

# Pivot
df_airport <- 
  df_flights %>% 
  pivot_longer(
    cols = terminal_arrive:terminal_outside,
    names_to = "area_stage", 
    values_to = "time") %>% 
  mutate(
    area  = stringr::str_split_fixed(area_stage, pattern = "_", n = 2)[,1],
    stage = case_when(
      stringr::str_detect(area_stage, "arrive") ~ "start",
      stringr::str_detect(area_stage, "clear") ~ "finish" )) %>% 
  filter(!is.na(stage)) %>% 
  select(-area_stage) %>% 
  pivot_wider(names_from = "stage", values_from = "time")

# Classify stages
df_airport <-
  df_airport %>%
  mutate(
    across(area, ~ stringr::str_to_title(.x)),
    area = fct_relevel(
      area,
      "Checkin",
      "Emmigration",
      "Security",
      "Gate",
      "Immigration",
      "Customs"),
    depart_arrive = if_else(as.numeric(area) < 15, "Departure", "Arrival"),
    time_taken = as.numeric(finish - start)) %>% 
  filter(!area %in% c("Terminal", "Gate"))

Graph Airport Areas

df_airport %>% 
  ggplot() +
  geom_boxplot(
    aes(x = time_taken/60, group = area, y = area), 
    colour = "#2B363B",
    size = .25,
    alpha = 0) +
  geom_jitter(
    aes(x = time_taken/60, group = area, y = area),
    colour = "#195080",
    height = 0.1, 
    alpha = 0.5,
    size = 2.5) +
  geom_text(
    data = . %>% group_by(area) %>% summarise(median = median(time_taken, na.rm = TRUE)),
    aes(x = median/60, label = round(median/60), y = area), 
    fontface = "bold",
    nudge_y = .5,
    size = 3) +
  scale_x_continuous(
    name = "Minutes") +
  scale_y_discrete(
    name = "Area of the Airport",
    limits = rev) +
  theme(
    axis.line.x = element_line(colour = "#2B363B"),
    axis.line.y = element_line(colour = "#2B363B"),
    axis.ticks = element_line(),
    panel.background = element_blank(), 
    panel.grid.major.x = element_line(colour = "#2B363B", size = .25),
    panel.grid.minor.x = element_line(colour = "#2B363B", size = .125))

Clean Arrivals Time

df_arrivals <- 
  df_flights %>% 
  select(-c(terminal_arrive:plane_lift_off)) %>% 
  filter(!is.na(plane_touchdown)) %>% 
  mutate(across(plane_touchdown:terminal_outside, ~ .x - plane_touchdown)) %>% 
  pivot_longer(
    cols = plane_touchdown:terminal_outside, 
    names_to = "area_stage", 
    values_to = "seconds") %>% 
  mutate(
    seconds = as.numeric(seconds),
    area_stage = fct_inorder(area_stage),
    area_stage = fct_recode(
      area_stage,
      "Plane Touchdown" = "plane_touchdown",
      "Seatbelts Off" = "plane_seatbelts_off",
      "Alighted Plane" = "gate_alighted",
      "Arrive Immigration" = "immigration_arrive",
      "Clear Immigration" = "immigration_clear",
      "Arrive Customs" = "customs_arrive",
      "Clear Customs" = "customs_clear",
      "Outside Terminal" = "terminal_outside")) %>% 
  filter(
    !is.na(seconds),
    !area_stage %in% c("Arrive Customs", "Clear Customs"))

Graph Arrivals Time

df_arrivals %>% 
  ggplot(aes(y = area_stage, x = seconds/60)) + 
  geom_boxplot(
    aes(group = area_stage),
    alpha = 0) +
  geom_point(
    aes(group = flight),
    colour = "#195080", 
    alpha = 0.5,
    size = 2.5) + 
  geom_line(
    aes(group = flight),
    linewidth = .125, 
    alpha = .25) +
  scale_x_continuous(
    name = "Minutes", 
    breaks = seq(0, 60, 10)) +
  scale_y_discrete(
    name = "Stage",
    limits = rev) +
  theme(
    axis.line.x = element_line(colour = "#2B363B"),
    axis.line.y = element_line(colour = "#2B363B"),
    panel.background = element_blank(), 
    panel.grid.major.x = element_line(colour = "#2B363B", size = .25),
    panel.grid.minor.x = element_line(colour = "#2B363B", size = .125))

Clean Seat Row Data

df_seats <- 
  df_flights %>% 
  mutate(
    row = as.numeric(stringr::str_sub(seat, 1, 2)),
    time_to_leave_plane = as.numeric(gate_alighted - plane_seatbelts_off)/60) %>% 
  select(flight, from, to, plane, row, time_to_leave_plane) %>% 
  filter(!is.na(time_to_leave_plane))

Graph Seat Row Time to Leave

df_seats %>% 
  ggplot(
    aes(
      y = time_to_leave_plane, 
      x = row)) + 
  geom_smooth(
    colour = "#2B363B",
    fullrange = TRUE, 
    method = "lm",
    se = TRUE) +
  geom_point(
    colour = "#195080", 
    alpha = 0.5,
    size = 5) + 
  scale_y_continuous(
    name = "Minutes to Leave Plane",
    expand = c(0, 0),
    limits = c(0, 17)) +
  scale_x_continuous(
    expand = c(0, 0),
    limits = c(0, 40)) +
  xlab("Seat Row") +
  theme(
    axis.line.x = element_line(colour = "#2B363B"),
    axis.line.y = element_line(colour = "#2B363B"),
    panel.background = element_blank(), 
    panel.grid.major.x = element_line(colour = "#2B363B", size = .25),
    panel.grid.minor.x = element_line(colour = "#2B363B", size = .125))