Building a TSA Wait Time Pipeline for 10 US Airports
AirWait tells travellers how long the security line is before they leave for the airport. That single feature turned out to be the hardest part of the app, for a reason I did not expect: there is no usable public API for TSA checkpoint wait times.
The TSA publishes historical averages. Travellers need the next hour. This is how I closed that gap for ten of the busiest airports in the United States, what worked, and what I had to give up.
Why the obvious approach fails
Every few months someone posts a "TSA wait times API" on a developer forum. They are almost always one of three things: a scrape of the TSA's historical CSV, a dead endpoint from an abandoned project, or a paid aggregator whose pricing assumes you are an airline.
The TSA's own published data is historical, aggregated by hour-of-day across past weeks. It will tell you that Atlanta averages 25 minutes at 7am on a Tuesday. It will not tell you that today there are two checkpoints closed and the line is 50 minutes.
So I went looking for who does have live data. The answer is the airports themselves.
Airports already display live wait times, on their own websites
Most large US airports show current security wait times on their own site. That number arrives in the browser somehow, which means there is an endpoint behind it.
Finding it is unglamorous work:
- Open the airport's security wait-time page with DevTools on the Network tab, filtered to XHR/Fetch.
- Reload and watch for a JSON response containing something that looks like minutes.
- If nothing appears, read the page's JavaScript bundle. Modern airport sites are often Next.js builds, and the endpoint is frequently sitting as a string literal in a chunk file.
- Replay the request from your own environment and see whether it survives outside the browser.
That last step is where most candidates died.
What ten airports actually yielded
I targeted the ten busiest: ATL, DFW, DEN, ORD, LAX, CLT, MCO, LAS, IAH and MIA. Two of them gave me a clean, reusable JSON endpoint.
Dallas/Fort Worth runs its queue data through a QLess deployment hosted on MuleSoft CloudHub. It returns a wait duration in minutes plus a queue state, so you can tell a real zero from a checkpoint that is simply closed. That distinction matters more than it sounds: a naive integration reports "0 minutes, walk right through" at 3am when the truth is "this checkpoint does not exist right now."
Miami uses SITA's aviation API, which returns an array of checkpoints, each with a projected maximum wait and an open/closed status. Miami has multiple checkpoints of different types, so the integration filters to the general-screening lanes that are actually open and takes the worst of them. Reporting the best lane would be technically accurate and practically useless, because travellers cannot choose their checkpoint from home.
The other eight failed, each in its own way:
- Atlanta sits behind a Cloudflare browser challenge. A server-side fetch never reaches the data. Solving that from a Cloud Function is neither reliable nor something I want to build a product on.
- Las Vegas and Orlando both expose real endpoints that work from a browser and time out from a server. Their gateways almost certainly want an Origin or Referer from their own domain. I could spoof those headers. I decided not to build a paying feature on a request that is one WAF rule change away from breaking silently.
- Charlotte serves wait times only through a chatbot widget. There is no clean data endpoint to call.
- Denver, O'Hare, Houston and LAX simply have no public JSON endpoint in their static HTML or JavaScript. The numbers are rendered server-side or not published at all.
Two out of ten. On a feature the app's core promise depends on.
The fallback is the product
This is the design decision I would defend hardest. When eight of ten sources are unavailable, you have two options: ship a feature that works in two cities, or make the absence of live data invisible to the traveller.
I built a prediction model that runs for every airport, including the two with live feeds. Each airport carries a baseline median wait — Atlanta 25 minutes, LAX 30, Las Vegas 18, and so on, derived from published historical data and differing by airport because the airports genuinely differ. The model then adjusts that baseline for time of day and season, because a 6am Monday and a 2pm Wednesday are not the same airport.
Live data, where it exists, overrides the prediction. Where it does not, the traveller still gets a number, and the app is honest about where the number came from.
The critical implementation detail: the prediction model exists twice, once in the Cloud Function and once on the device, and the two must agree exactly. The iOS app can predict locally when it is offline or when Firestore is cold. If the two implementations drift, the same airport shows different numbers depending on which code path answered, and that is a bug users notice immediately and never forgive. Every change to the model has to land in both places in the same commit.
Adding the third source: travellers themselves
The app lets users report the wait they actually experienced. Those reports land in Firestore, and a trigger recomputes a rolling average for that airport and writes it back to the airport's document.
Crowdsourced data has an obvious cold-start problem and a less obvious poisoning problem, so it is treated as one input among three rather than as truth. But when it works, it beats everything else, because it is the only source measuring the thing travellers actually care about: how long you will stand in line, not how long the checkpoint's queue-management vendor thinks its queue is.
The shape of the pipeline
The whole thing is deliberately boring:
- A scheduled Cloud Function runs hourly and walks all ten airports.
- For each one it attempts the live endpoint if it has one, falls back to the prediction if it does not, and writes a single document per airport with the value and its source.
- The iOS app reads those documents directly through the Firestore SDK. There is no REST API in between, no server to keep alive, and no custom endpoint to secure.
- A separate Firestore trigger folds in user reports as they arrive.
Hourly is a deliberate choice. Security waits do not move meaningfully minute to minute, and a scheduled function that runs 24 times a day costs approximately nothing. Polling ten airports every five minutes would multiply my costs and my rate-limit exposure by twelve to make the data fresher than the underlying reality.
Writing to Firestore rather than serving an API also means the phone never waits on an airport's gateway. If DFW's endpoint is slow, that slowness happens in a background function on a schedule, not in front of a user who is trying to decide when to leave for the airport.
What I would tell someone starting this
Check whether your data source exists before you design around it. I assumed live wait times were available because airports display them. Two out of ten were actually consumable. Had I sequenced the work differently, I would have found that out in an afternoon instead of after building the integration layer.
Store the provenance of every value. Every wait time in the system knows whether it came from a live feed, a prediction or a user report. That field costs nothing and it is what makes the difference between a number and a number you can debug.
Do not scrape what you can predict. I could have run a headless browser to defeat Atlanta's Cloudflare challenge. It would have worked, and it would have broken, quietly, on a Tuesday, without telling me. A prediction that is 80% right and never breaks was worth more than a scrape that is 95% right until it is 0% right.
An endpoint that works in your browser has not been proven to work from a server. This one cost me the most time. Las Vegas and Orlando both looked like wins in DevTools and both failed from a Cloud Function. Replay every candidate request from the environment that will actually make it, before you write the integration.
One note on responsible use: the endpoints these airports serve to their own front-ends are public in the sense that any browser can reach them, but they are not published APIs with terms I have agreed to. I call them at a low, fixed rate, I cache aggressively, and I degrade to prediction rather than retry hard when one is unhappy. I would not publish their credentials, and I would not build a business that depends on their goodwill without a conversation first.
Building something with awkward data sources?
Most interesting mobile features run into exactly this problem: the data you need exists but nobody publishes it cleanly. I have shipped this pattern end to end.
Talk to me about your project