This week, King Khalid International Airport in Riyadh was attacked more than once. The Houthis claimed missile strikes, and Saudi Arabia's civil aviation authority said the attacks on 8 October killed three people, including a pilot. The airport closed and reopened within a day.
It's the latest disruption in a year of them. When US and Israeli strikes on Iran began on 28 February, Gulf states closed their airspace. Emirates, Etihad and Qatar Airways suspended flights, and thousands of passengers were stranded in Dubai, Abu Dhabi, Doha and elsewhere. The effects are still showing: by 10 August, WINGX was reporting private-jet departures from Gulf countries 46.5% below their pre-war level.
For dispatchers, operations teams, travel platforms and anyone moving people or cargo through a region like this, the question is the same: how do you find out quickly, and reliably, that airspace has closed? This guide covers the three signals that tell you, what each one is good for, and how to combine them into a monitor.
One thing to be clear about first: this is situational awareness. Operators planning flights need their authority's official briefing products and their own risk assessment. The tools here help you notice, prioritise and explain. They don't replace the official sources.
What "airspace closure" actually means
"The airspace is closed" covers several different things, and they're published in different ways.
A whole FIR closed. A flight information region is the block of airspace a country's air traffic control is responsible for. When a state closes its FIR, nobody routes through it, and traffic has to go around. Iran, Iraq and several Gulf states closed theirs at the end of February.
Routes or areas closed. More often, part of the airspace closes: particular routes, or a restricted area activated at short notice. Traffic can still pass, but on different routes and often with delays.
An airport closed. The airspace may stay open while an airport shuts, as Riyadh did this week. Arrivals divert, departures wait.
Advice to operators. Separately, aviation authorities warn their own airlines about other countries' airspace. The FAA does it with international NOTAMs filed under the location KICZ and with Special Federal Aviation Regulations that ban US operators from specific areas. In Europe, EASA publishes Conflict Zone Information Bulletins. An airspace can be technically open while most Western airlines avoid it because of these.
The first three appear in NOTAMs. The last appears in authority publications, and you'll mostly see its effect in traffic patterns.
Signal 1: NOTAMs, the official word
Closures are published as NOTAMs, and the NOTAM endpoint returns a structured record for each one. The key field is the Q-code, five letters that encode what the notice is about and what has happened to it. Letters two and three say what's affected, and four and five say what's changed:
| Q-code | Meaning |
|---|---|
QAFLC | FIR closed |
QARLC | ATS route closed |
QFALC | Aerodrome closed |
QMRLC | Runway closed |
QRTCA | Temporary restricted area activated |
QRRCA | Restricted area activated |
QRPCA | Prohibited area activated |
QRDCA | Danger area activated |
Matching on Q-codes is far more reliable than searching the free text, which varies from country to country. FAA domestic NOTAMs often have no Q-code, though, so a text fallback is still needed:
import requests
BASE = "https://skylink-api.p.rapidapi.com"
H = {"X-RapidAPI-Key": KEY, "X-RapidAPI-Host": "skylink-api.p.rapidapi.com"}
def get(path, **params):
r = requests.get(f"{BASE}{path}", headers=H, params=params, timeout=30)
r.raise_for_status()
return r.json()
# Q-code subject (letters 2-3) + condition (letters 4-5)
CLOSURE_CODES = {
"AFLC": "FIR closed",
"ARLC": "ATS route closed",
"FALC": "Aerodrome closed",
"MRLC": "Runway closed",
}
ACTIVATED_AREAS = {"RT": "Temporary restricted area", "RR": "Restricted area",
"RP": "Prohibited area", "RD": "Danger area"}
WHAT = ("AIRSPACE", "FIR", "AERODROME", "AD CLSD", "ROUTE")
SHUT = ("CLSD", "CLOSED", "PROHIBITED")
def classify(notam):
q = (notam.get("q_code") or "").upper()
if len(q) == 5 and q[0] == "Q":
if q[1:] in CLOSURE_CODES:
return CLOSURE_CODES[q[1:]]
if q[1:3] in ACTIVATED_AREAS and q[3:] == "CA":
return ACTIVATED_AREAS[q[1:3]] + " active"
return None
# FAA domestic NOTAMs often have no Q-code, so fall back to the text
body = (notam.get("body") or "").upper()
if any(w in body for w in WHAT) and any(s in body for s in SHUT):
return "Closure (from text)"
return None
def closures(icao):
data = get(f"/notams/{icao}", include_future="true", exclude_qcode="QK")
out = []
for n in data["notams"]:
kind = classify(n)
if kind:
out.append({"id": n.get("notam_id"), "kind": kind, "scope": n.get("scope"),
"fir": n.get("affected_fir"), "from": n.get("effective"),
"to": n.get("expiration"), "status": n.get("status"),
"limits": (n.get("lower_limit"), n.get("upper_limit")),
"text": n.get("body")})
return outA few details matter here:
include_future=truereturns NOTAMs that have been published but haven't started yet, taggedFUTURE. Closures are often announced a few hours ahead, and that's the warning you want.exclude_qcode=QKdrops checklist NOTAMs on the server, so you don't waste time on them.scopeisAERODROMEfor airport notices andFIRfor en-route airspace. The endpoint is queried by airport, and FIR-scope NOTAMs come back with anaffected_fir. To watch a country's airspace, poll its main airports and look at the FIR-scope results.- Altitude limits matter. A restricted area from the surface to 10,000 feet affects arrivals and departures, not overflights at 37,000. Keep
lower_limitandupper_limit, and show them.
For the FIR codes you're likely to watch in the Gulf region: OIIX is Tehran, ORBB Baghdad, OBBB Bahrain, OMAE the Emirates and OEJD Jeddah, which covers Saudi Arabia. Q-codes are covered in more depth in decoding NOTAMs programmatically.
NOTAMs have limits as an early warning. In a sudden event, the NOTAM can follow the closure by some time, and it may be worded as a short, general notice. NOTAMs tell you a closure is official. They don't always tell you first.

Signal 2: live traffic, what's actually happening
When airspace closes, the sky empties. That's visible in live ADS-B data within minutes, often before any NOTAM, and it shows you what airlines are really doing rather than what's officially allowed.
The method is to count aircraft above 10,000 feet in a box and compare the count with the same hour of the week in previous weeks:
import statistics
from collections import defaultdict
from datetime import datetime, timezone
history = defaultdict(list) # (region, hour of week) -> counts seen in previous weeks
def count_aircraft(bbox):
data = get("/adsb/aircraft", bbox=bbox, min_alt=10_000)
return len(data["aircraft"])
def traffic_check(region, bbox, now=None):
now = now or datetime.now(timezone.utc)
slot = (region, now.weekday() * 24 + now.hour)
count = count_aircraft(bbox)
past = history[slot]
verdict = None
if len(past) >= 3:
normal = statistics.median(past)
if normal >= 20 and count < 0.4 * normal:
verdict = f"{region}: {count} aircraft above 10,000 ft, normally about {normal:.0f}"
past.append(count)
del past[:-8] # keep eight weeks
return verdict
traffic_check("Gulf", "22,48,30,57") # bbox: SW corner, then NEComparing with the same hour of the week matters, because traffic over the Gulf has strong daily and weekly patterns. The night bank of departures from Dubai and Doha looks nothing like mid-afternoon. The normal >= 20 guard stops the check firing on boxes that are always quiet. In production, keep the history in a database rather than in memory.
Treat a traffic drop as a strong hint, and read it alongside the other signals. Two things can make a drop look worse than it is:
- Coverage gaps. ADS-B depends on receivers, and coverage over some conflict areas is thinner than over Europe.
- GPS interference. Jamming and spoofing are common near conflict zones and can produce bad positions or make aircraft vanish from feeds for a while. How GPS jamming shows up in ADS-B tracks explains what to look for.
Signal 3: reroutes, around the closure
When a FIR closes, flights don't stop. They go around it, and that shows up as aircraft flying far from their normal route. The great circle from Dubai to London passes close to Baghdad. With Iranian and Iraqi airspace closed, a flight has to go around, for example over Saudi Arabia and Egypt. Over the northern Red Sea, it's about 1,200 km from that great circle.
You can detect that by comparing each aircraft's position with the great circle between its departure and arrival airports:
import math
from functools import lru_cache
R = 6371.0
def _xyz(lat, lon):
p, l = math.radians(lat), math.radians(lon)
return (math.cos(p)*math.cos(l), math.cos(p)*math.sin(l), math.sin(p))
def off_track_km(dep, arr, pos):
"""Distance from `pos` to the great circle through dep and arr."""
a, b, p = _xyz(*dep), _xyz(*arr), _xyz(*pos)
n = (a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0])
norm = math.sqrt(sum(c*c for c in n))
return abs(R * math.asin(sum(nc*pc for nc, pc in zip(n, p)) / norm))
@lru_cache(maxsize=None)
def airport_pos(icao):
a = get("/airports/search", icao=icao)
return (a["latitude_deg"], a["longitude_deg"])
def rerouted(plane, threshold_km=400):
"""Flag an aircraft flying far from the great circle of its usual route."""
route = get(f"/routes/callsign/{plane['callsign'].strip()}")
if route.get("confidence") != "high":
return None # no reliable route for this callsign
dep, arr = airport_pos(route["departure_icao"]), airport_pos(route["arrival_icao"])
off = off_track_km(dep, arr, (plane["latitude"], plane["longitude"]))
return round(off) if off > threshold_km else NoneThe 400 km threshold is deliberately loose. Normal routes rarely follow the great circle exactly, and airways, winds and traffic flows can move a flight 100 to 200 km off it on an ordinary day. Callsign routes only count when the match is high-confidence, because a guessed route would produce false reroutes.
One flagged flight tells you little. Twenty flights on different routes all bending around the same area tells you the airspace in the middle is closed or being avoided, whatever the NOTAMs say.

Signal 4: the departure boards
For travel platforms, the question that matters most is often whether flights are leaving at all. The schedules endpoint returns departure boards with a free-text Status, and a rising share of cancellations at a hub is unmistakable:
def cancelled_share(iata):
flights = get("/schedules/departures", iata=iata)["flights"]
if not flights:
return None
cancelled = sum("cancel" in f["Status"].lower() for f in flights)
return cancelled / len(flights)The keys are title case (Status, not status), and the status is display text, so match loosely. A cancellation share that jumps from a few percent to most of the board at Dubai, Doha or Riyadh is as clear as signals get.
Putting it together
Each signal answers a different question:
| Signal | Answers | Speed | Weakness |
|---|---|---|---|
| NOTAMs | Is it official, and what are the limits? | Minutes to hours | Can lag a sudden event |
| Traffic counts | Is the sky emptying? | Minutes | Coverage gaps, jamming |
| Reroutes | Are airlines avoiding an area? | Minutes | Needs reliable routes |
| Departure boards | Are flights operating? | Minutes | Free-text statuses |
A reasonable monitor runs all four for the regions you care about. NOTAMs and boards every few minutes for key airports, traffic counts every five to ten minutes, and the reroute check on aircraft in surrounding areas. Raise an alert when two signals agree, such as a traffic drop plus a closure NOTAM, or a cancellation spike plus reroutes. A single signal on its own is worth a look, not a page at 3am.
Keep a timeline. In the first hours of a crisis, someone will ask when the airspace closed and when flights started moving again, and the answer should come from your logs.
Running it within your plan
Polling costs requests. Checking NOTAMs and departure boards for four airports every five minutes comes to about 70,000 requests a month, well over the Pro plan's 20,000. So use two speeds. In normal times, check NOTAMs every 30 minutes and boards every 15, which is about 17,000 requests a month for four airports, and run the traffic count less often. Switch the affected region to the fast schedule only once something triggers, and respect your plan's per-second rate limit when you do.
Where to go next
The NOTAM side is covered in more depth in decoding NOTAMs programmatically and, for US airspace, temporary flight restrictions. For the live data, the ADS-B tracking page covers what the feed includes, and how GPS jamming shows up explains the interference you'll see near conflict zones.
You can try the NOTAM API and live ADS-B on the free trial, which is for personal, non-commercial use. A monitor for a business needs a paid plan.
