Airflow ETL Pipeline: Stock Data to SQLite End to End
Build a full Airflow ETL from REST API to SQLite with TaskFlow, pandas, and connections.
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
- ✓TaskFlow basics with @dag and @task
- ✓Pandas flatten and datetime handling
- ✓An Airflow connection for the source API
- An Airflow ETL pipeline extracts from a REST API, transforms with pandas, and loads to SQLite or Postgres through Hooks
- Key components are TaskFlow extract, pandas flatten, Hook-based load with upsert, and Connections for secrets
- Performance insight: pandas flatten of 50,000 Polygon rows runs in 9 seconds; chunked loads keep memory under 400MB
- Production insight: hardcoded API keys leak in git; Connections with env backends keep secrets out of DAG code
An ETL pipeline is like running a small grocery: you fetch fresh produce from suppliers, clean and sort it in the back room, then stock the shelves for shoppers. Airflow is the store manager who makes sure fetching, cleaning, and stocking happen in order every day, and who keeps the supplier passwords locked in a safe instead of taped to the register.
You need stock prices in a database every morning. The API works, the transform works, but the pieces run by hand and the key sits in the code.
You'll build it as three tasks that run in order. Extract with a connection, flatten with pandas, load with a Hook.
We follow the Polygon-to-SQLite pattern: design table first, wire the graph, hide secrets properly. You'll have a runnable DAG by the end.
No keys in code. Ever.
Design First: Source, Destination, Transformation Plan
Write the tech spec before code: source endpoint and rate limit, destination table and key, transform rules for flattening nested JSON.
For Polygon stocks the grain is one row per symbol per date with an upsert key on (symbol, date). That choice makes reruns safe from day one.
Extract: Authenticated REST Call as a Task
Extract reads credentials from a Connection at runtime and returns raw JSON. No parsing here, just fetch and return.
Keep the task atomic: one symbol batch per run, with retries on 429 and 5xx. Pagination lives inside this task, not across tasks.
Give the call its own request timeout (30 seconds is sane) separate from retries, and declare retries on the decorator like @task(retries=3, retry_delay=timedelta(minutes=5)) so 429s and 5xx back off instead of dying. Write raw payloads to staging storage like S3 or /tmp keyed by ds and return the path, not the payload; XComs are for pointers, not 50,000-row dumps.
Transform: JSON Flattening With Pandas
Flatten nested API payloads into a typed frame with explicit columns. Rename vendor fields to warehouse names in one place.
Coerce dtypes and reindex to expected columns so schema drift fails fast. A 50,000-row Polygon payload flattens in about 9 seconds.
Use @task(multiple_outputs=True) when transform returns a dict so each key becomes its own XCom instead of one opaque blob; downstream can then pull order_summary['total_order_value'] by name. Receive context explicitly with typed args like ds or ti instead of **kwargs soup; it's faster and self-documenting.
Load: Hook Plus Upsert
Loads must be idempotent: rerunning the same interval replaces the same rows. Use INSERT OR REPLACE on SQLite or ON CONFLICT on Postgres.
Create the table if missing, batch the executemany, and return the loaded count for lineage. Single-writer SQLite needs a Pool with one slot.
Wiring Dependencies and the Graph View
TaskFlow wires dependencies through function calls: load(flatten(extract())) draws a clean three-node graph. No manual bitshifts needed.
Check Graph view for extract, flatten, load in sequence. Each node should show one responsibility and typed inputs in the logs.
Mix styles freely: classic operators expose .output for TaskFlow calls, and decorated tasks accept .override(task_id=..., retries=...) for reuse across DAGs. When one task needs alien dependencies, reach for @task.virtualenv or @task.external_python instead of polluting the worker image; Docker or Kubernetes decorators isolate the truly exotic. Need context deep in a helper? get_current_context() fetches it without threading kwargs through every layer.
Secrets Through Connections, Never Code
Define polygon_default in UI or env as AIRFLOW_CONN_POLYGON_DEFAULT with the token in password. Dev uses env files, prod uses Vault.
Scan every PR with gitleaks and block merges on hits. Rotate by updating the backend; the DAG code never changes.
The API Key That Ended Up in the Repo
- Secrets live in Connections and backends, never in DAG files or git — don't trust a private repo to stay private.
- Every ETL needs extract, transform, and load as separate atomic tasks.
- CI must scan for keys; tutorials inline them but production never should.
airflow connections get polygon_defaultairflow tasks test stocks_etl extract_prices manual__2026-09-01| File | Command / Code | Purpose |
|---|---|---|
| dags | from airflow.decorators import dag, task | Extract |
| verify-etl.sh | airflow dags show stocks_etl | Wiring Dependencies and the Graph View |
Key takeaways
Common mistakes to avoid
4 patternsHardcoding the API key in the DAG
Using plain INSERT for loads
Flattening without schema checks
Heavy top-level imports and network calls
Interview Questions on This Topic
How do you structure a production ETL DAG in Airflow?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
That's Airflow. Mark it forged?
3 min read · try the examples if you haven't