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.

GTFS ingestion pipeline stages Five sequential boxes connected by arrows: Fetch ZIP, Parse CSV, Validate integrity, Normalize times and coordinates, Store Parquet or database. Fetch ZIP archive Parse CSV + dtypes Validate refs + schema Normalize time + CRS Store Parquet / DB agency URL pandas / pyarrow FK + bounds IANA tz + WGS84 partitioned

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:

  1. Metadata and service definitionsagency.txt, feed_info.txt, calendar.txt, calendar_dates.txt
  2. Network topologyroutes.txt, trips.txt, stops.txt, shapes.txt
  3. Temporal schedulesstop_times.txt, frequencies.txt

The relational spine runs agency.txtroutes.txttrips.txtstop_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:

GTFS entity-relationship diagram Six entity boxes — agency.txt, routes.txt, trips.txt, stop_times.txt, stops.txt, calendar.txt — connected by foreign-key arrows showing the relational spine of a GTFS feed. agency.txt PK: agency_id routes.txt PK: route_id trips.txt PK: trip_id stop_times.txt FK: trip_id, stop_id stops.txt PK: stop_id calendar.txt PK: service_id agency_id route_id trip_id stop_id service_id

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.txt encodes recurring weekly patterns: which days of the week a service runs, and the date window it is valid within.
  • calendar_dates.txt encodes exceptions: exception_type=1 adds a service on a specific date the weekly pattern omits; exception_type=2 removes 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 keystrip_id values in stop_times.txt with no matching row in trips.txt. These trips are invisible to routing but consume memory during ingestion. Orphaned records must be quarantined and logged, not silently dropped.
  • Missing service coverageservice_id values in trips.txt that appear in neither calendar.txt nor calendar_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_sequence values or duplicate (trip_id, stop_sequence) pairs in stop_times.txt. Both cause undefined ordering behavior in stop-time queries.
  • Sub-24-hour clock wrapping — agencies recording 00:30:00 instead of 24:30:00 for an overnight trip break service-day continuity. Detection requires comparing departure_time across successive stops for monotonicity violations.
  • Zero or out-of-range coordinatesstop_lat or stop_lon values of 0.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_date expiry 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:

  1. Fetch the ZIP from the agency URL or a feed registry; verify SHA-256 against the last known content hash
  2. Extract to a temporary directory; parse feed_info.txt for version and validity window
  3. Parse all mandatory files with explicit dtype declarations; enforce str for all ID columns to prevent silent numeric coercion on digit-only identifiers
  4. Validate referential integrity: confirm the full trip_id chain, service_id coverage, stop_sequence monotonicity, and coordinate bounds
  5. Resolve the service calendar: merge calendar.txt and calendar_dates.txt into a concrete (service_id, date) set
  6. Apply timezone normalization per agency; convert stop_times.txt values to UTC after calendar resolution, not before
  7. Expand frequency-based trips from frequencies.txt into explicit departure times if the file is present
  8. Compute spatial enrichments using appropriate CRS projections: project stops to a local CRS, snap to shapes, compute route centroids
  9. Write Parquet partitioned by agency_id and feed_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 int coercion on ID columnspandas will infer trip_id as int64 if the column contains only digits, silently converting leading-zero identifiers and breaking joins with string-typed keys in other files. Always pass explicit dtype={'trip_id': str, 'stop_id': str, 'route_id': str, 'service_id': str}.
  • Calendar gaps — an agency publishes a feed_end_date but stops updating calendar.txt entries 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_sequence under different distances — agencies occasionally publish the same stop_sequence value twice under a single trip_id with different shape_dist_traveled values. Both rows pass FK validation but produce undefined ordering.
  • Shape-stop misalignmentshapes.txt geometry 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:00 on 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.txt departure counts inflated by stop_times.txt rowsstop_times.txt rows for frequency-based trips contain relative offsets, not absolute departures. Counting those rows as actual departures double-counts headway-based service.
  • Disconnected station graphsstops.txt includes both platform stops (location_type=0) and parent stations (location_type=1) but omits the parent_station foreign 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.


← Home