GTFS Feed Architecture & Fundamentals
The General Transit Feed Specification (GTFS) is the de facto interchange format for public transportation schedules, network topology, and service metadata. Transit analysts, routing engineers, and mobility platform teams all depend on the same ZIP-packaged CSV files — but the structural decisions embedded in those files have outsized consequences for ingestion pipelines, routing accuracy, and operational dashboards. A malformed foreign key in trips.txt, a naively converted timezone offset, or an undetected shape gap can cascade into wrong ETAs, failed routing queries, and customer-facing misinformation.
This guide covers the complete engineering picture: the layered data model, the spatial and temporal conventions that differ from most data formats, the validation rules that catch real-agency quirks before they reach production, and the Python tooling that makes the whole workflow reproducible at scale.
End-to-End Pipeline Overview
Every production GTFS workflow follows the same logical stages, regardless of how many agencies or how large the feed. Understanding where each GTFS file enters the pipeline prevents the common mistake of performing timezone conversion before resolving service calendars, or validating shapes before referential integrity is confirmed.
Each stage gates the next. Normalization on top of unvalidated foreign keys produces silently wrong output. Storing before timezone resolution produces trips that land on the wrong calendar day for multi-agency aggregation. The pipeline must be sequential and each stage must be idempotent so failed runs can safely restart.
Core Data Model & File Relationships
GTFS is a normalized relational dataset serialized as CSV. The full static feed structure resolves into three logical layers:
- Metadata and service definitions —
agency.txt,feed_info.txt,calendar.txt,calendar_dates.txt - Network topology —
routes.txt,trips.txt,stops.txt,shapes.txt - Temporal schedules —
stop_times.txt,frequencies.txt
The relational spine runs agency.txt → routes.txt → trips.txt → stop_times.txt, with stops.txt and calendar.txt hanging off the join. Violations anywhere in that chain cascade into downstream failures.
| File | Primary key | Key foreign keys | Purpose |
|---|---|---|---|
agency.txt |
agency_id |
— | Operator name, timezone, contact |
routes.txt |
route_id |
agency_id |
Logical service lines (bus, rail, ferry) |
trips.txt |
trip_id |
route_id, service_id, shape_id |
Individual vehicle runs per service day |
stops.txt |
stop_id |
parent_station |
Physical boarding and alighting points |
stop_times.txt |
— | trip_id, stop_id |
Ordered arrival/departure sequence per trip |
calendar.txt |
service_id |
— | Recurring weekly service patterns |
calendar_dates.txt |
— | service_id, date |
Holiday exceptions and one-off service |
shapes.txt |
— | shape_id |
WGS84 polyline for each vehicle path |
feed_info.txt |
— | — | Publisher, version, validity window |
frequencies.txt |
— | trip_id |
Headway-based service (no fixed departure times) |
The stop_times.txt and stops.txt relationship is the computational core of any GTFS feed. Every routing query, ETA prediction, and isochrone calculation ultimately resolves through a join on trip_id and stop_id. Because stop_times.txt grows quadratically with network size and service frequency, it is also the primary memory and I/O bottleneck in Python-based ingestion. Production pipelines index on trip_id and stop_sequence immediately after parsing, before any spatial or temporal enrichment.
The entity-relationship structure illustrates the full dependency chain:
Spatial Representation & Coordinate Reference Systems
GTFS mandates WGS84 (EPSG:4326) decimal degrees for all geographic fields: stop_lat and stop_lon in stops.txt, and shape_pt_lat and shape_pt_lon in shapes.txt. At least six decimal places are required for sub-meter precision — coordinates rounded to four decimal places introduce errors of up to 11 metres, enough to misplace a stop across a street or onto the wrong platform.
The full implications of coordinate reference systems for transit data become apparent the moment you project into local systems for GIS analysis. Planar approximations of WGS84 coordinates distort distances at high latitudes and near the antimeridian. When computing stop buffers, shape snapping, or route centroids in geopandas, always project to an appropriate equal-area or conformal CRS for the region before spatial operations, then project back to WGS84 for output.
Transit agencies frequently publish stops at curb positions rather than platform centroids. This creates a systematic offset when routing engines expect network-aligned node positions. Shape accuracy varies more widely: some agencies publish shapes traced at lane resolution; others publish straight lines between timepoints. Both cases require explicit handling in ingestion pipelines — either accepting the approximation with documented tolerances, or applying a map-matching step to snap shapes to a road network.
Temporal Normalization & Schedule Handling
GTFS time representation departs from conventional clock semantics in one critical way: hours beyond 23:59:59 are valid. A trip departing at 25:30:00 leaves at 01:30 AM the following calendar day, but belongs to the prior day’s service_id. This design keeps overnight service tied to a single service record without splitting trips across midnight boundaries.
All times in stop_times.txt are expressed relative to the agency’s declared IANA timezone in agency.txt. For multi-agency aggregators, timezone handling and schedule normalization cannot be applied globally — each agency’s times must be resolved against its own declared timezone before any cross-agency comparisons. Applying a single UTC offset to all records is the single most common cause of misaligned ETAs in multi-agency routing systems.
The service calendar logic splits across two files:
calendar.txtencodes recurring weekly patterns: which days of the week a service runs, and the date window it is valid within.calendar_dates.txtencodes exceptions:exception_type=1adds a service on a specific date the weekly pattern omits;exception_type=2removes a service on a date the weekly pattern would otherwise include.
Merging these two tables to produce a concrete list of (service_id, date) pairs is a prerequisite for any schedule expansion. The merge must happen before timezone conversion, because DST transitions alter the UTC offset for times on specific dates — resolving 25:30:00 on a DST-transition date requires knowing the date first.
For agencies operating frequency-based schedules via frequencies.txt, the temporal model changes further: stop_times.txt rows for those trips are relative offsets from the trip’s start time, not absolute departure times. Converting these to exact departure times requires a separate expansion step before standard schedule queries will work.
Validation, Referential Integrity & Error Handling
GTFS feeds from real agencies contain errors. The validation pipeline is not optional — it is the difference between a routing engine that returns confident wrong answers and one that surfaces problems before they reach users.
Validation rules and common schema errors cover the full taxonomy, but the most operationally significant failure modes are:
- Orphaned foreign keys —
trip_idvalues instop_times.txtwith no matching row intrips.txt. These trips are invisible to routing but consume memory during ingestion. Orphaned records must be quarantined and logged, not silently dropped. - Missing service coverage —
service_idvalues intrips.txtthat appear in neithercalendar.txtnorcalendar_dates.txt. The affected trips have no valid service dates and will never be selected by a routing query, yet they inflate trip counts. - Sequence gaps and duplicates — non-sequential
stop_sequencevalues or duplicate(trip_id, stop_sequence)pairs instop_times.txt. Both cause undefined ordering behavior in stop-time queries. - Sub-24-hour clock wrapping — agencies recording
00:30:00instead of24:30:00for an overnight trip break service-day continuity. Detection requires comparingdeparture_timeacross successive stops for monotonicity violations. - Zero or out-of-range coordinates —
stop_latorstop_lonvalues of0.0,null, or outside the valid ±90/±180 range. Zero-coordinate stops cluster near the Gulf of Guinea and break any spatial query.
Automated validation should cover three stages: schema parsing (field types, required columns present), referential integrity (foreign key existence across all files), and semantic compliance (value ranges, temporal monotonicity, shape continuity). Failing fast at the parsing stage avoids wasting compute on downstream transformations that will produce garbage on broken input.
Metadata, Versioning & Feed Governance
feed_info.txt is optional in the GTFS specification, but its absence creates significant operational friction. Without feed_start_date, feed_end_date, and a stable feed_version field, downstream systems must heuristically determine whether a newly fetched ZIP represents a substantive update or an identical re-publication. ZIP modification timestamps are unreliable — many agencies re-upload unchanged content on a fixed schedule.
Agency metadata and feed versioning practices covers the full governance model. At a minimum, production pipelines should:
- Compute a content hash (SHA-256 of the extracted file contents, not the ZIP wrapper) and use that as the primary change-detection key
- Store every historical feed version in object storage, keyed by agency ID and content hash, to support backfills and debugging of routing regressions
- Track
feed_end_dateexpiry and raise alerts before the active feed window closes — many agencies publish updates only a few days before the prior feed expires - Implement schema drift detection to flag when a new feed version adds or removes columns, even if referential integrity otherwise passes
Multi-agency aggregators should treat each agency’s feed as a separate data product with its own SLA and quality thresholds. A single degraded feed should not block ingestion for the rest of the network.
Automation and Python Tooling
Python’s ecosystem covers every stage of the GTFS pipeline. The choice of library has significant memory and performance implications for large feeds:
| Library | Best use | Memory profile | Typical use case |
|---|---|---|---|
pandas |
General-purpose parsing and transformation | High (full in-memory) | Feeds under 200 MB; exploratory analysis |
pyarrow |
Zero-copy reads, Parquet I/O | Low (columnar, lazy) | Large feeds; Parquet output pipelines |
partridge |
Strict FK enforcement, filtered loading | Medium | Production ingestion with referential integrity checks; see parsing GTFS with partridge |
gtfs-kit |
High-level feed analysis, map output | Medium-high | Exploratory analysis, automated feed updates; see automating feed updates with gtfs-kit |
geopandas |
Spatial joins, coordinate projection | High (geometry overhead) | Stop clustering, shape snapping, route geometry extraction |
For memory-efficient processing of large feeds, the most effective strategies are chunked reading of stop_times.txt, categorical dtype encoding for trip_id and stop_id columns (which reduces memory footprint by 60–80% on typical feeds), and early filtering by service_id to drop trips outside the target date window before joining.
A production ingestion pipeline follows this sequence:
- Fetch the ZIP from the agency URL or a feed registry; verify SHA-256 against the last known content hash
- Extract to a temporary directory; parse
feed_info.txtfor version and validity window - Parse all mandatory files with explicit
dtypedeclarations; enforcestrfor all ID columns to prevent silent numeric coercion on digit-only identifiers - Validate referential integrity: confirm the full
trip_idchain,service_idcoverage,stop_sequencemonotonicity, and coordinate bounds - Resolve the service calendar: merge
calendar.txtandcalendar_dates.txtinto a concrete(service_id, date)set - Apply timezone normalization per agency; convert
stop_times.txtvalues to UTC after calendar resolution, not before - Expand frequency-based trips from
frequencies.txtinto explicit departure times if the file is present - Compute spatial enrichments using appropriate CRS projections: project stops to a local CRS, snap to shapes, compute route centroids
- Write Parquet partitioned by
agency_idandfeed_date; publish to internal catalog and trigger routing index rebuild
Common Failure Modes
These are the errors that appear most frequently in production feeds from real agencies — not theoretical spec violations, but bugs that slip through agency QA processes:
- Silent
intcoercion on ID columns —pandaswill infertrip_idasint64if the column contains only digits, silently converting leading-zero identifiers and breaking joins with string-typed keys in other files. Always pass explicitdtype={'trip_id': str, 'stop_id': str, 'route_id': str, 'service_id': str}. - Calendar gaps — an agency publishes a
feed_end_datebut stops updatingcalendar.txtentries two weeks earlier. Trips exist in the feed for the gap period, no validation error is raised, and date-filtered routing queries return zero results for those days. - Duplicate
stop_sequenceunder different distances — agencies occasionally publish the samestop_sequencevalue twice under a singletrip_idwith differentshape_dist_traveledvalues. Both rows pass FK validation but produce undefined ordering. - Shape-stop misalignment —
shapes.txtgeometry snapped to an old road network while stops reflect newer infrastructure. The mismatch may be small enough to pass coordinate bounds checks but large enough to break accessibility routing or turn-by-turn navigation. - DST-boundary trips producing wrong UTC times — resolving
25:30:00on a DST transition date without a date-aware timezone library produces a UTC offset that is off by one hour. Only libraries that accept a(time_string, date, timezone_name)triple and return a timezone-aware datetime handle this correctly. frequencies.txtdeparture counts inflated bystop_times.txtrows —stop_times.txtrows for frequency-based trips contain relative offsets, not absolute departures. Counting those rows as actual departures double-counts headway-based service.- Disconnected station graphs —
stops.txtincludes both platform stops (location_type=0) and parent stations (location_type=1) but omits theparent_stationforeign key on platform rows. Transfer queries fail silently because the station hierarchy is incomplete.
What Robust Implementation Looks Like at Scale
A production-grade GTFS system treats feeds as versioned data products, not ad-hoc CSV downloads. Each feed version is immutable once stored; transformations are deterministic and reproducible from the raw ZIP; validation gates block promotion to routing indexes until integrity thresholds are met; and alerting covers feed expiry, schema drift, and orphaned-record rates as first-class operational signals.
The Python parsing and data normalization techniques that implement these principles — from pandas dtype strategies through multi-agency batch processing and memory optimization for feeds exceeding 500 MB — form the implementation complement to the architectural patterns covered here. As GTFS-Realtime integration becomes the logical next layer for transit platforms, the static feed architecture described in this guide forms the reference baseline that real-time vehicle positions and trip updates are merged against.
Frequently Asked Questions
What files are mandatory in a GTFS static feed?
The mandatory files are agency.txt, routes.txt, trips.txt, stops.txt, and stop_times.txt. Either calendar.txt or calendar_dates.txt (or both) must also be present to define service days. All other files — shapes.txt, frequencies.txt, feed_info.txt, transfers.txt — are optional but commonly expected by routing engines and data pipelines.
Why do GTFS times exceed 24:00:00?
GTFS expresses all times relative to the service day’s local noon minus 12 hours, not the calendar midnight. This lets overnight trips (e.g. a bus departing at 25:30:00) stay tied to a single service_id without crossing into the next calendar day, preventing ambiguity around midnight-boundary service.
What coordinate system does GTFS use?
All geographic coordinates use WGS84 (EPSG:4326) decimal degrees. A minimum of six decimal places is recommended for sub-meter positioning accuracy.
Related
- Understanding GTFS Static Feed Structure — file inventory, required vs optional fields, and how routing engines consume the feed
- Mastering stop_times.txt and stops.txt Relationships — join strategies, sequence validation, and memory optimization for the largest GTFS table
- Coordinate Reference Systems for Transit Data — WGS84, local projections, spatial joins, and shape snapping
- Timezone Handling and Schedule Normalization — IANA timezone resolution, DST transitions, multi-agency UTC alignment
- GTFS Validation Rules and Common Schema Errors — full taxonomy of spec violations, referential errors, and remediation workflows
- Python Parsing and Data Normalization — cross-pillar:
pandas,partridge,gtfs-kit, and production ingestion patterns