A journey planner makes its hardest decisions before anyone types an address. OpenTripPlanner has to turn street geometry, platforms, calendars, stop times, transfers, elevation, and live disruptions into one searchable transport model. Only then can it answer the apparently simple question on a rider's screen: how do I get there?
That makes OpenTripPlanner less like a map widget and more like a timetable compiler. It builds a durable graph from open datasets, keeps the resulting model in memory, applies a moving layer of real-time changes, and searches for several defensible itineraries rather than one mathematically ordained “best” route. The architecture is valuable because it makes each boundary inspectable: static data versus live data, streets versus transit, build-time policy versus request-time preference, and technically possible transfers versus humane ones.[1][6][7]
Image context: the cover photograph shows a train and airport shuttle at Torp station in Norway. Entur uses OpenTripPlanner for nationwide public-transport journey planning, so this is not generic mobility decoration; it is the physical transfer reality that the software's data contracts must describe.[6][8]
The first product is a graph artifact
OpenTripPlanner starts with two different accounts of a city. OpenStreetMap describes the walkable and rideable fabric: streets, paths, crossings, access restrictions, steps, and the geometry around stops. A transit feed describes service through time. In the common GTFS form, that schedule begins with seven core text files for agencies, routes, trips, stops, stop times, weekly calendars, and calendar exceptions. Optional files add pathways, transfers, fares, and other rider-facing detail.[1][3]
Those inputs are not queried raw for every request. OTP's build-config.json controls a graph build that imports and links them into a prepared artifact. The distinction is operationally important: settings such as the OSM sources, transit feeds, service period, elevation sampling, island pruning, and transfer precomputation affect what goes into the graph. Change one of those assumptions and the safe response is usually a new build, not a clever runtime patch.[2]
The configuration model says as much. Build parameters are stored in the graph itself, and OTP can embed router configuration with that artifact. A build-level configVersion can be injected by a deployment pipeline so operators can identify the build-configuration revision a server loaded; if the pipeline ties that value to pinned source snapshots, it also becomes useful provenance.[2] That is a small feature with outsized value. When two instances disagree about a route, “same application version” is not enough; they may have different OSM extracts, feed snapshots, service windows, or build settings.
This also explains the compiler metaphor. A build can succeed technically while producing a semantically broken graph. A stop may be stranded from the street graph. An unstable or incorrectly assigned feedId can break the join between static and real-time data.[11] A footpath may be missing, a platform may be on the wrong side of a barrier, or a calendar may extend absurdly far into the future. OTP can produce an import report through dataImportReport, but the report only becomes useful when someone owns it as a release gate.[2] A successful graph build means the files were processed. It does not yet mean the resulting journeys make sense.
A transfer is where the models meet
Transit routing lives at the seam between time and space. A GTFS trip can say that a train reaches a stop at 21:47 and a shuttle leaves another stop at 21:53. OpenStreetMap can say that the stops are 170 meters apart. Neither fact alone establishes a viable transfer. The answer also depends on entrances, stairs, platform geometry, crossings, access rules, minimum transfer time, and whether the stops are correctly linked to station and street geometry.[3][13]
This is why transfer quality is the fastest way to judge an OTP deployment. Direct rides can look plausible even with mediocre data. Transfers expose every join. A missing station pathway creates an impossible teleport or an unnecessarily long detour. A station centroid placed across a fence can turn six minutes into fiction.[13] A feed's in-seat block_id can mean a rider should remain on the same vehicle even though the public route number changes.[2][3] Accessibility adds another graph: elevators, slopes, stairs, platform access, and the uncomfortable category of “unknown.”[12][13]
The Torp photograph makes that seam visible. The train and airport bus are close enough to fit in one frame, but a useful itinerary still needs the correct stop pair, service calendars, walk link, direction, buffer, and live status. Route planning is not the act of drawing a polyline between them. It is the act of proving that the handoff survives contact with the platform.
For an engineering team, the practical test corpus should therefore be made of awkward journeys, not landmarks. Include the station with two similarly named stops, the last connection of the evening, a trip that crosses midnight, a wheelchair route when an elevator is marked inaccessible or its status is unknown, an in-seat continuation, and a transfer whose legal walking path looks longer than the straight line. These cases reveal the data model much faster than a hundred uncomplicated downtown searches.[12]
RAPTOR searches a frontier, not a single truth
Once the model exists, OTP 2 isolates its transit search engine around the RAPTOR algorithm. The project's architecture notes describe a narrow interface: OTP maps its own context into a RAPTOR request, the routing engine searches timetable rounds, and the result is mapped back into OTP's domain. Transfer optimization and itinerary filtering then help turn the search output into a useful list.[4]
The separation is more than code hygiene. “Fastest” is not a complete definition of a good transit journey. One itinerary may arrive earlier but require another transfer. Another may leave later, preserve a safer connection, or reduce walking. Entur's production explanation of its nationwide service describes a Pareto search across arrival time, departure time, transfers, and accumulated generalized cost. An option survives when it is not simply worse on every relevant dimension; later filtering removes alternatives that would be technically distinct but unhelpful to riders.[6]
This is where product policy enters the engine. OTP's runtime configuration exposes defaults and costs for walking, waiting, boarding, transfers, accessibility, rental modes, and itinerary filtering. The defaults are not neutral facts about transport. They encode judgments about friction. A two-minute walk and a two-minute wait occupy the same clock time but need not feel equivalent. A nominally accessible stop with unknown elevator status should not necessarily rank like a verified step-free path.[9]
The architecture keeps those judgments changeable without rebuilding the street-and-schedule graph. router-config.json can alter routing defaults and server behavior at startup, while individual API requests can override exposed preferences. That is the clean division: the build establishes the baseline world, updaters attach live changes, and routing policy searches and ranks paths through it.[2][5][9]
The danger is tuning by screenshot. Penalizing transfers until one familiar commute “looks right” can silently suppress useful journeys elsewhere. A better practice is to version ranking policy, replay a representative query set, and compare candidate sets and filter-debug output before and after a change. If an itinerary disappears, the team should know whether it became impossible, lost on generalized cost, or was removed by a filter.[4][9]
Real time is a patch layer, not a second world
Scheduled data supplies the baseline. Real-time feeds describe deviations: delays, changed stop times, cancellations, alerts, and vehicle positions. OTP adds these after graph construction through updaters configured in the updaters section of router-config.json. For GTFS Realtime, separate updater types can poll alerts, trip updates, and vehicle positions; streaming trip updates over MQTT are also supported.[5]
For the common scheduled-trip path, that structure preserves a vital join: an update needs the right feed and trip identity in the static model. A skipped stop needs consistent time handling. A delay with missing downstream estimates needs an explicit propagation policy. OTP's configuration exposes those choices, including whether matching may be fuzzy and how delays are carried forward or backward when the feed is incomplete.[5]
This is the most common place to overstate “real-time routing.” A moving vehicle dot is not proof that itineraries are current. Vehicle positions, service alerts, and TripUpdates—which carry predictions and cancellations—can arrive at different cadences and fail independently. If the static schedule rolled over but the live feed still references yesterday's trip identifiers, fresh messages can become unusable. If a poller is healthy but the producer has stopped advancing timestamps, the pipeline can be green while the passenger view is stale.[5]
Operators therefore need freshness and match-rate metrics beside ordinary uptime. Track the age of each feed, accepted and rejected updates, unknown trip IDs, updater failures, graph age, and the share of searches that actually include real-time changes. Test a cancellation and a skipped stop end to end. The architectural question is not “did we ingest protobuf?” It is “did a changed vehicle operation alter the rider's itinerary in the intended way?”
The operational boundary is memory
OTP's deployment profile follows directly from its model: required data is held in memory. The project's system guidance says footprints range from less than 1 GB to more than 100 GB depending on geography and network density; its examples put a Finland-wide dataset a little above 10 GB and Germany around 95 GB. Single-thread performance and CPU cache matter for an individual request, while additional cores help serve concurrent requests and run update work.[7]
Those numbers rule out “just put it in a tiny container” planning. Geography is a capacity decision. So are elevation data, service horizon, street detail, and how many regional feeds are combined. The graph build may also have a different peak-memory profile from the running router, which argues for separating build jobs from serving instances and promoting a tested artifact rather than rebuilding casually on every pod start.
There are other useful guardrails. OTP's documented router defaults allocate for 12 transfers and set a 24-hour upper limit on request-specified search windows; the docs caution that larger request windows can produce inconsistent results and performance trouble because of how timetable data is collected before a RAPTOR search.[9] These are not universal service promises. They are reminders that a journey planner has explicit search budgets, even when the public interface feels limitless.
The pilot should be deliberately small
OpenTripPlanner fits organizations that can own transport data as an operational product: agencies, regional platforms, research teams, and mobility services with reliable OSM or street inputs, standardized schedules, and a reason to control routing policy and APIs. Entur's service shows the upper end of that fit: one backend combines static and dynamic sources for public transport and mobility across Norway, then exposes journey planning through a Transmodel-shaped GraphQL API.[6][10]
It is a weaker fit when a team wants an embeddable route library, has no one responsible for feed quality, or expects software to repair ambiguous stops and broken calendars automatically. Open source removes a license boundary; it does not remove the transport-domain work. A national graph with silent transfer errors is still a bad journey planner.
A disciplined pilot is modest. Select one bounded region. Pin the OTP release, source snapshots, and configuration version. Build the graph once in a repeatable job. Review import issues. Create a query corpus around difficult transfers and accessibility. Add one real-time feed and measure freshness plus match rate. Load-test the production-shaped machine, not a developer laptop. Only then widen the geography or add modes. A 2026 Australian research deployment used matching metropolitan OSM extents, a repeatable trimming workflow, and concurrent-load tests for the same reason: geographic scope and production behavior are part of the experiment, not cleanup after it.[14]
The payoff is substantial. When the data and operations are treated seriously, OpenTripPlanner turns open standards into public infrastructure that can be inspected from input file to returned itinerary. Its deepest architectural insight is that route finding is the last step. First, the system has to compile a believable world.
Sources
- OpenTripPlanner Project, “OpenTripPlanner 2,” version 2.9 documentation overview covering supported modes, GraphQL APIs, open-data inputs, real-time updates, licensing, and deployment scope.
- OpenTripPlanner Project, “Graph Build Configuration,” version 2.9 documentation for
build-config.json, graph artifacts, source URIs, service windows, import reports, transfer preparation, and embedded configuration. - General Transit Feed Specification, “Overview,” official explanation of GTFS Schedule and GTFS Realtime, including the seven core schedule files and optional extensions.
- OpenTripPlanner Project, “Architecture,” source-tree architecture index covering domain models, the isolated RAPTOR component, transfer optimization, and itinerary filtering.
- OpenTripPlanner Project, “GTFS-RT,” version 2.9 updater documentation for alerts, trip updates, vehicle positions, polling, MQTT streaming, trip matching, and delay propagation.
- Entur, “How does Journey Planner v3 actually work?,” operator documentation on Norway's OpenTripPlanner-backed service, its static and dynamic inputs, RAPTOR search, and Pareto criteria.
- OpenTripPlanner Project, “System Requirements and Suggestions,” version 2.9 deployment guidance on in-memory graph size, processor behavior, concurrent requests, and production machine examples.
- Wolfmann, “Torp train station, NSB shuttle bus to Sandefjord Lufthavn airport,” Wikimedia Commons, real photograph made on March 20, 2019 and used as this article's image.
- OpenTripPlanner Project, “Router Configuration,” version 2.9 documentation for routing defaults, web-request overrides, RAPTOR transfer allocation, search windows, server behavior, and itinerary costs.
- Entur, “Journey Planner v3,” operator API documentation describing the OpenTripPlanner-backed national service and its Transmodel-based GraphQL endpoint.
- OpenTripPlanner Project, “Feed ID,” version 2.9 documentation on feed-scoped identifiers and the static-to-real-time association established during graph building.
- OpenTripPlanner Project, “Accessibility,” version 2.9 documentation on unknown and inaccessible wheelchair data, elevator treatment, routing costs, and accessible transfer precomputation.
- OpenTripPlanner Project, “In-station navigation,” version 2.9 documentation on stop coordinates, OSM paths, GTFS pathways, platform transfers, buffers, and common data errors.
- Kiki Adhinugraha, Yusuke Gotoh, and David Taniar, “An Open-Source System for Public Transport Route Data Curation Using OpenTripPlanner in Australia,” Computers 15(1), 2026 — an independent OTP 1.5 deployment study used here only for spatial-scope, reproducibility, and load-testing practice.