Building a Real-Time Flight Data Dashboard with Python and Redis
Thousands of aircraft are in the sky at this very moment, each one broadcasting its position, altitude, and speed via a short-range radio signal. Ground-based receivers pick up those signals and feed them into public APIs. With Python, Redis, and Streamlit you can pull that live data, cache it sensibly, and render a real map of airborne traffic right in your browser. This tutorial walks through that exact project, from the first pip install to a working live dashboard.
Modern commercial and private aircraft use ADS-B transponders to broadcast GPS-derived positional data continuously on the 1090 MHz frequency. A global network of volunteer-operated receivers aggregates those broadcasts and makes them available through public APIs. If you want a deeper understanding of how those signals travel from transponder to API endpoint, flight tracking technology as a field covers the hardware, receiver architecture, and protocol chain involved. For this project, what matters practically is that the data is real, updated every few seconds, and free to access.
Pre-Flight Checklist
- The OpenSky Network provides a free REST API returning real-time ADS-B state vectors for thousands of airborne aircraft
- Redis caching with a short TTL prevents your app from tripping API rate limits on every Streamlit script rerun
- Each state vector includes latitude, longitude, barometric altitude, velocity, and callsign per aircraft
- PyDeck’s
ScatterplotLayerrenders aircraft positions as interactive dots on a globe inside Streamlit - The full stack runs locally with Docker for Redis and five Python packages
What the OpenSky Network Actually Gives You
The OpenSky Network is a non-profit research consortium that operates a global network of ADS-B receivers. Their public REST API requires no API key for anonymous access. You send a GET request to a single endpoint and receive a JSON payload containing a states array. Each entry in that array is a fixed-length list representing one aircraft currently tracked by the network.
Fields include the ICAO24 transponder address (a unique hex identifier assigned to each aircraft), the callsign, origin country, last position timestamp, longitude, latitude, barometric altitude, velocity in metres per second, true heading, and whether the aircraft is currently on the ground. The complete state vector specification in the official API docs lists every field by index and data type, which you will need when writing your parser.
Anonymous users can call the API roughly once per ten seconds. That cadence is frequent enough for a live dashboard. Creating a free account raises the limit further, but the anonymous tier works well for a solo project running on a single machine.
Why Redis Is the Right Tool for Rate-Limit Safety
Streamlit reruns the entire Python script on every user interaction. Every slider adjustment, button click, or widget change triggers a fresh execution from top to bottom. Without a cache layer, each rerun would hit the OpenSky API directly. You would exceed the rate limit within seconds during normal use.
Redis solves this with a pattern that is almost embarrassingly simple. Cache the last successful API response under a fixed key, set a TTL (time-to-live) that matches the API’s rate window, and serve every app request from the cache until that TTL expires. The API call only fires when the cache is empty or stale. The rest of the time, your app reads from memory at sub-millisecond latency.
There is a second benefit specific to this architecture. Flight data does not need to be fresher than ten seconds for a tutorial dashboard. Caching aggressively means users get fast responses even on slow networks, and the data stays accurate enough to be genuinely interesting to watch.
Setting Up Your Environment
You need Python 3.9 or later, a running Redis instance, and five packages. The fastest path to a local Redis server is Docker:
docker run -d -p 6379:6379 redis:7-alpine
That command pulls the official Redis image and binds it to port 6379. Then install the Python dependencies:
pip install streamlit redis requests pydeck pandas
Each package has a specific role in this project:
- streamlit handles the browser UI, widget state management, and the polling refresh loop
- redis is the Python client that communicates with your local Redis instance
- requests makes the HTTP calls to the OpenSky REST endpoint
- pydeck renders aircraft positions as an interactive scatter layer on a 3D globe
- pandas converts the raw state vector lists into a structured, filterable DataFrame
Writing a Redis Cache Layer That Respects API Limits
Create a file called cache.py. The logic is deliberately minimal: check Redis first, call the API only on a cache miss, write the result back with a TTL, and return the data either way.
import redis
import requests
import json
import os
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
CACHE_KEY = "opensky:states"
CACHE_TTL = 12 # slightly above the 10-second anonymous rate limit
r = redis.from_url(REDIS_URL)
def get_states():
cached = r.get(CACHE_KEY)
if cached:
return json.loads(cached)
url = "https://opensky-network.org/api/states/all"
resp = requests.get(url, timeout=10)
resp.raise_for_status()
data = resp.json()
r.setex(CACHE_KEY, CACHE_TTL, json.dumps(data))
return data
redis.from_url reads the connection string from an environment variable, making the module portable without code edits. setex writes the key and sets the TTL in a single atomic operation, which prevents a race condition where two concurrent Streamlit sessions both find the cache empty and both fire API requests at the same instant.
Parsing State Vectors into a Pandas DataFrame
The states field from OpenSky is a list of lists. Column names are positional rather than named in the JSON. A dedicated parser keeps that field-index knowledge in one place and hands the rest of the app a clean, labeled DataFrame.
import pandas as pd
COLUMNS = [
"icao24", "callsign", "origin_country", "time_position",
"last_contact", "longitude", "latitude", "baro_altitude",
"on_ground", "velocity", "true_track", "vertical_rate",
"sensors", "geo_altitude", "squawk", "spi", "position_source"
]
def parse_states(data: dict) -> pd.DataFrame:
states = data.get("states") or []
df = pd.DataFrame(states, columns=COLUMNS)
df = df[df["longitude"].notna() & df["latitude"].notna()]
df = df[~df["on_ground"]].copy()
df["latitude"] = df["latitude"].astype(float)
df["longitude"] = df["longitude"].astype(float)
df["baro_altitude"] = pd.to_numeric(
df["baro_altitude"], errors="coerce"
).fillna(0)
return df
The on_ground filter drops parked aircraft, keeping your map focused on planes actually in flight. The NaN check on coordinates removes aircraft that have a valid transponder contact but have not yet reported a GPS position. Both filters run before the type conversions, avoiding casting errors on rows with missing data.
Building the Live Map with Streamlit and PyDeck
Create app.py. Streamlit’s st.rerun() paired with time.sleep() creates a polling loop that refreshes the map automatically without requiring any user interaction between cycles.
import streamlit as st
import pydeck as pdk
import time
from cache import get_states
from parser import parse_states
st.set_page_config(page_title="Live Flight Map", layout="wide")
st.title("Live Flight Positions")
refresh_rate = st.sidebar.slider("Refresh interval (seconds)", 10, 60, 15)
placeholder = st.empty()
while True:
data = get_states()
df = parse_states(data)
layer = pdk.Layer(
"ScatterplotLayer",
data=df,
get_position=["longitude", "latitude"],
get_radius=15000,
get_fill_color=[255, 140, 0, 200],
pickable=True,
)
view = pdk.ViewState(latitude=20, longitude=0, zoom=1.5)
deck = pdk.Deck(
layers=[layer],
initial_view_state=view,
tooltip={
"text": "{callsign}\nAlt: {baro_altitude}m | Speed: {velocity}m/s"
},
)
with placeholder.container():
st.pydeck_chart(deck)
st.caption(
f"Tracking {len(df)} airborne aircraft. "
f"Next refresh in {refresh_rate}s."
)
time.sleep(refresh_rate)
st.rerun()
The placeholder.container() pattern replaces the map in-place on each cycle rather than appending a new chart below the previous one. Without it, every rerun would stack a fresh pydeck chart on the page and your layout would grow indefinitely. The sidebar slider gives live control over the refresh cadence during a session without touching the source code.
Running the Dashboard and Checking Your Setup
With Redis running and your files saved, start the app from your project directory:
streamlit run app.py
Your browser opens to http://localhost:8501 and a dark globe covered in orange dots appears within a few seconds. Each dot is a real aircraft currently tracked by the OpenSky receiver network. Hovering over a dot shows the callsign, altitude, and velocity from the tooltip configuration. The caption at the bottom updates the aircraft count on every cycle.
If the map loads but shows no dots, add a temporary st.write(df.head()) call immediately after parse_states to inspect the DataFrame. Empty results usually mean either the API returned an error or the on_ground filter removed everything because the snapshot you fetched happened to contain no airborne aircraft with valid coordinates. You can also call st.write(data.get("states", "no states key")) to confirm the raw API response arrived correctly.
To verify Redis is working independently, run redis-cli ping in a separate terminal. A PONG response confirms the connection. During a live session, redis-cli ttl opensky:states shows the remaining TTL on the cached key, counting down to zero and then resetting when the next API call fires.
Where This Stack Takes You After the First Flight
The dashboard you have built is a working foundation, and the architecture extends naturally. Add a geographic bounding box to the OpenSky API request (the endpoint accepts lamin, lamax, lomin, and lomax query parameters) and the response payload shrinks to a single region, cutting both parse time and Redis storage. Store a snapshot to a second Redis key on a one-minute schedule using setex with a longer TTL, and you have enough history to replay a route over time. Wire up a check that watches for a specific ICAO24 address in the cached state vector and writes an alert to a third key when it appears, and you have a personal arrival notification system with very little additional code.
The underlying pattern, fetch at the API boundary, cache in Redis, render in Streamlit, is not specific to flight data. The same three-layer structure handles weather feeds, financial tick data, sports scores, or any high-frequency data source with rate limits. Flight data just happens to produce one of the more visually compelling results when the dots start moving across a globe in real time.