GTFS-RT
The node downloads a GTFS Realtime feed (Protocol Buffers) in a worker thread, decodes it as a stream, filters the entities by the stop, route and trip ids it has learned from the GTFS node and emits trip updates (TripUpdates), vehicle positions (VehiclePositions) and service alerts (Alerts) in batches through three outputs. It is designed for very large, Germany-wide feeds.
- Node type
- gtfs-realtime
- Package
- @htag/node-red-contrib-hypertegrity-urbanmobility
- Inputs / outputs
- 1 / 3
- Data source
- Any GTFS Realtime feed as Protobuf via HTTP(S) (default: realtime.gtfs.de)
When to use this node
GTFS Realtime complements the static GTFS schedule with real-time information. A feed is a binary Protobuf message (FeedMessage) with three kinds of entities: TripUpdates with delays and changed arrival and departure times per stop, VehiclePositions with GPS position, bearing and speed of the vehicles, and Alerts with service disruptions, their cause and effect. Germany-wide feeds contain tens of thousands of entities per fetch. The GTFS-RT node reduces this volume to the stops, routes and trips of your area, taking their ids from the outputs of the GTFS node.
Typical use cases are:
- Departure boards with current delays for the stops of a municipality.
- Live maps with vehicle positions of the local bus and rail routes.
- Disruption notices on dashboards or in notification flows.
How the node works
Learn filters
Messages with GtfsStop, GtfsRoute or GtfsTrip entities from the GTFS node replace the respective filter list. They do not trigger a fetch and are persisted to disk.
Trigger
Any other message starts the fetch, provided at least one filter list or msg.stopIds exists.
Decode as a stream
A worker thread downloads the feed (gzip and deflate are decompressed) and decodes the Protobuf entities piece by piece without blocking the Node-RED event loop.
Filter and emit
Entities matching a learned trip, route or stop id are sent in batches to the outputs TripUpdates, VehiclePositions and Alerts.
Result: Three outputs with trip updates, vehicle positions and service alerts for your area, in batches of configurable size.
The node is trigger-based. An Inject node with an interval of one to five minutes is common; many feeds are updated every 30 to 60 seconds. The learned filter lists are stored as a file in the temporary directory of the operating system under node-red-gtfs-realtime/<node id>/filters.json, survive deploys and restarts and are deleted when the node is removed from the flow.
Configuration
The edit dialog has no tabs.
| Field | Type | Default | Meaning |
|---|---|---|---|
| Name | Text | empty | Display name of the node in the flow. |
| Feed URL | Text | see below | Address of the GTFS Realtime Protobuf file. http:// and https:// are supported, redirects are followed. |
| Batch Size | Number | 1000 | Maximum number of entities per output message and output. As soon as one output type reaches this number, all three batches are sent. |
Default values of the URL fields:
Feed URL: https://realtime.gtfs.de/realtime-free.pb
Filtering is done by ids delivered by the GTFS node or passed in msg.stopIds.
Input
The node distinguishes two kinds of messages by their payload.
| Property | Type | Meaning |
|---|---|---|
msg.payload | array of entities with id starting with urn:ngsi-ld:Gtfs | Filter data from the GTFS node. Depending on type (GtfsStop, GtfsRoute, GtfsTrip) the content completely replaces the list of stop, route or trip ids. Agency and StopTime entities are ignored. No fetch is triggered. |
msg.payload | any other value | Triggers the fetch of the feed. |
msg.stopIds | array of strings | Additional stop ids merged with the learned list. Allows a fetch even without a connected GTFS node. |
msg.topic | string | Carried over to the output messages. The output messages contain topic, payload, feedTimestamp and totalEntities. |
Ids are sanitised for comparison as in the GTFS node (all characters other than letters, digits, _ and - become _), so that a trip id de:vrn:12-345:67 from the feed matches the learned id de_vrn_12-345_67.
Output
Every output delivers messages with an array in the payload plus msg.feedTimestamp (timestamp of the feed, ISO 8601) and msg.totalEntities (entities of the feed read so far). An output receives a message only if its batch contains at least one entity. Timestamps are converted from Unix seconds to ISO 8601, enumeration values are translated to their names.
Output 1: TripUpdates
A trip update matches if its trip id, its route id or a stop id in stopTimeUpdate belongs to the filter lists.
[
{
"id": "tu-20261001-0815-105",
"type": "GtfsRtTripUpdate",
"trip": {"tripId": "de:vrn:12-345:67", "routeId": "de:vrn:12-345", "directionId": 0, "startTime": "08:15:00", "startDate": "20261001", "scheduleRelationship": "SCHEDULED"},
"vehicle": {"id": "4711", "label": "105", "licensePlate": null},
"stopTimeUpdate": [
{"stopSequence": 5, "stopId": "de:07312:1001", "scheduleRelationship": "SCHEDULED",
"arrival": {"delay": 120, "time": "2026-10-01T06:19:00.000Z", "uncertainty": 0},
"departure": {"delay": 120, "time": "2026-10-01T06:20:00.000Z", "uncertainty": 0}}
],
"timestamp": "2026-10-01T06:17:40.000Z",
"delay": 120
}
]
| Field | Meaning |
|---|---|
id, type | Entity id from the feed and always GtfsRtTripUpdate. |
trip | Trip id, route id, direction, start time and start date (YYYYMMDD) plus scheduleRelationship (SCHEDULED, ADDED, CANCELED, …). |
vehicle | Vehicle id, label and licence plate if present in the feed, otherwise null. |
stopTimeUpdate | Per stop: sequence, stop id, status and arrival and departure with delay in seconds, time and uncertainty. |
timestamp, delay | Time of the report and overall delay of the trip in seconds. |
Output 2: VehiclePositions
A vehicle position matches if its trip id, route id or current stop id belongs to the filter lists.
[
{
"id": "vp-4711",
"type": "GtfsRtVehiclePosition",
"trip": {"tripId": "de:vrn:12-345:67", "routeId": "de:vrn:12-345", "directionId": 0, "startTime": "08:15:00", "startDate": "20261001", "scheduleRelationship": "SCHEDULED"},
"vehicle": {"id": "4711", "label": "105", "licensePlate": null},
"position": {"latitude": 49.4447, "longitude": 7.7689, "bearing": 270, "speed": 8.3},
"location": {"type": "Point", "coordinates": [7.7689, 49.4447]},
"currentStopSequence": 5,
"stopId": "de:07312:1001",
"currentStatus": "IN_TRANSIT_TO",
"timestamp": "2026-10-01T06:18:10.000Z",
"congestionLevel": "UNKNOWN"
}
]
| Field | Meaning |
|---|---|
position | Latitude, longitude, bearing in degrees and speed in metres per second (according to the GTFS-RT specification). |
location | The same position as a GeoJSON point [longitude, latitude]. |
currentStopSequence, stopId, currentStatus | Next or current stop and status INCOMING_AT, STOPPED_AT or IN_TRANSIT_TO. |
congestionLevel | Traffic situation from UNKNOWN to SEVERE_CONGESTION. |
trip, vehicle, timestamp | As for TripUpdates. |
Output 3: Alerts
A service alert matches if one of its informed entities (informedEntity) names a learned stop, route or trip id.
[
{
"id": "alert-2026-1042",
"type": "GtfsRtAlert",
"activePeriod": [{"start": "2026-10-01T04:00:00.000Z", "end": "2026-10-03T20:00:00.000Z"}],
"informedEntity": [{"routeId": "de:vrn:12-345"}],
"cause": "CONSTRUCTION",
"effect": "DETOUR",
"url": "https://www.example-transit.org/alerts/1042",
"headerText": "Diversion route 105",
"descriptionText": "Due to roadworks the stop Market Square is not served."
}
]
| Field | Meaning |
|---|---|
activePeriod | Validity periods of the alert. |
informedEntity | Affected agencies, routes, modes, stops or trips. |
cause, effect | Cause (STRIKE, WEATHER, CONSTRUCTION, …) and effect (NO_SERVICE, DETOUR, SIGNIFICANT_DELAYS, …). |
url, headerText, descriptionText | Link, headline and description in the first language contained in the feed. |
Example flow
The package ships with an example flow. Import it in Node-RED via Menu → Import → Examples → @htag/node-red-contrib-hypertegrity-urbanmobility → gtfs-realtime. Alternatively download gtfs-realtime.json and import it via Menu → Import. The example flow contains two GTFS nodes with the boundary polygon of Kaiserslautern; the abridged version below shows one node with a placeholder.
GTFS learns the ids
A daily Inject node triggers the GTFS node; its Stops, Routes and Trips outputs are wired to the input of the GTFS-RT node.
GTFS-RT fetches every 5 minutes
A second Inject node triggers the fetch of the real-time feed from realtime.gtfs.de.
Three Debug nodes
One Debug node each shows TripUpdates, VehiclePositions and Alerts in the sidebar.
Show example flow as JSON
[
{"id": "tab1", "type": "tab", "label": "gtfs-realtime", "disabled": false, "info": ""},
{"id": "inject1", "type": "inject", "z": "tab1", "name": "01:00 daily", "props": [{"p": "payload"}], "repeat": "", "crontab": "00 01 * * *", "once": false, "onceDelay": "30", "topic": "", "payload": "", "payloadType": "date", "x": 190, "y": 200, "wires": [["gtfs1"]]},
{"id": "gtfs1", "type": "gtfs-data", "z": "tab1", "name": "gtfs.de", "url": "https://download.gtfs.de/germany/free/latest.zip", "refreshInterval": "23", "stopTimesBatchSize": 50000, "areas": "[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[\"<POLYGON-COORDINATES>\"]]},\"properties\":{\"display_name\":\"Kaiserslautern, Rheinland-Pfalz, Deutschland\",\"osm_id\":62652,\"osm_type\":\"relation\"}}]", "x": 330, "y": 200, "wires": [[], ["rt1"], ["rt1"], ["rt1"], []]},
{"id": "inject2", "type": "inject", "z": "tab1", "name": "5 min", "props": [{"p": "payload"}], "repeat": "300", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 170, "y": 340, "wires": [["rt1"]]},
{"id": "rt1", "type": "gtfs-realtime", "z": "tab1", "name": "", "url": "https://realtime.gtfs.de/realtime-free.pb", "batchSize": 1000, "x": 500, "y": 340, "wires": [["debug1"], ["debug2"], ["debug3"]]},
{"id": "debug1", "type": "debug", "z": "tab1", "name": "TripUpdates", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 720, "y": 300, "wires": []},
{"id": "debug2", "type": "debug", "z": "tab1", "name": "VehiclePositions", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 730, "y": 340, "wires": []},
{"id": "debug3", "type": "debug", "z": "tab1", "name": "Alerts", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 700, "y": 380, "wires": []}
]
Status and errors
| Indicator | Meaning |
|---|---|
| Yellow ring "Starting worker..." | The worker thread is loading the Protobuf schema. Triggers during this time are queued and executed after start-up. |
| Grey ring "Filter: 412 stops, 38 routes, 9850 trips" | Idle state with an overview of the learned filter lists. Without filter lists the status stays empty. |
| Blue dot "412 stops loaded", "38 routes loaded", "9850 trips loaded" | A filter list was received from the GTFS node and stored. |
| Yellow ring "Streaming: 25000" | The feed is being read; the number names the entities decoded so far. |
| Green dot "Ready (312 entities)" | The fetch is complete; the number names the emitted entities of all three outputs. |
| Yellow ring "No filter loaded - connect a gtfs-data node first" | There are neither learned filter lists nor msg.stopIds; the fetch is refused and reported as a warning. |
| Warning "Already fetching..." | A fetch is still active; the new trigger is discarded. |
| Red ring "Error" | The fetch failed, for example "HTTP 404", "Request timeout" (after 60 seconds), "Proto schema not loaded yet" or a decoding error. |
| Warning "Worker exited with code 1" | The worker thread ended unexpectedly; the node restarts it after 2 seconds. |
| Warning "Filter cache load failed: …" or "Filter cache save failed: …" | The filter file could not be read or written. |
Errors are reported via node.error and can be handled with a Catch node.
Notes
- Working with the GTFS node: Wire outputs 2 (Stops), 3 (Routes) and 4 (Trips) of the GTFS node to the input of this node. Every delivery replaces the respective list completely; running the GTFS node once a day is sufficient. Because the lists are persisted to disk, no new GTFS run is needed after a restart.
- Rate limits: The default feed from gtfs.de is freely accessible; observe the terms of use and polling intervals stated there. Every fetch downloads the complete feed, several megabytes for all of Germany.
- Large data volumes: Do not use the "complete message object" setting on Debug nodes and limit the display; batches of 1000 entities can strain the browser. The batch size controls how many entities are sent per message.
- Units: Delays in seconds, speed in metres per second, bearing in degrees, coordinates in decimal degrees (WGS 84).
- Personal data: Vehicle positions and the fields
vehicle.id,vehicle.labelandvehicle.licensePlateallow an assignment to individual vehicles and thereby indirectly to driving staff. Store such data only as long as necessary, omit licence plates from publications and check whether vehicle identifiers should be removed or pseudonymised.