The implementation provided by this service supports the GTFS-Realtime data exchange format with the following types of information:
GTFS-Realtime uses Google Protocol Buffers: a language and platform-neutral mechanism for efficient
serialization of structured data (smaller and faster than XML). The data model is defined in the
gtfs-realtime.proto file, which declares all message
element hierarchies and types (e.g., FeedMessage, FeedHeader, FeedEntity).
All feeds are provided as Protocol Buffer binary payloads (Content-Type: application/x-protobuf) over HTTP.
Consumers request specific feeds by supplying a feed label (configured logical key) and a feed type
(Alerts, TripUpdates, or VehiclePositions).
Pattern:
GET /GtfsProtoBuf?FeedLabel={feedKey}&FeedType={Alerts|TripUpdates|VehiclePositions}
Feeds are refreshed at least every 30 seconds, or sooner when underlying data (e.g., vehicle position)
changes. If no entity content changes, a new FeedHeader.timestamp is still emitted to assert
that the snapshot remains valid at that moment.
Internal pruning logic may remove stale entities (expired alerts, inactive vehicles, completed trips) before serialization to keep payloads relevant and lean.
Each response supplies a Last-Modified header derived from the feed header's UNIX timestamp
(converted to RFC1123 format). Clients are encouraged to send If-Modified-Since on subsequent
requests; if the timestamp has not advanced, the service returns 304 Not Modified (no body),
conserving bandwidth.
This conditional model helps both producers and consumers avoid redundant parsing and transmission of unchanged binary payloads.
The GTFS-Realtime protocol buffer payloads are served by the /GtfsProtoBuf endpoint. Each request
returns a binary FeedMessage (as defined in gtfs-realtime.proto) with
Content-Type: application/x-protobuf.
GET /GtfsProtoBuf?FeedLabel={feedKey}&FeedType={feedType}
FeedLabel: Case-insensitive key identifying the configured feed.FeedType: One of Alerts, TripUpdates, VehiclePositions (case-insensitive)./GtfsProtoBuf?FeedLabel=regional&FeedType=TripUpdates
/GtfsProtoBuf?FeedLabel=regional&FeedType=VehiclePositions
/GtfsProtoBuf?FeedLabel=agencyA&FeedType=Alerts
200 OK – Body is the serialized FeedMessage bytes.Content-Type: application/x-protobufLast-Modified header reflects the feed header timestamp converted to RFC1123.
If a client sends If-Modified-Since and the feed's timestamp has not advanced, the service
returns 304 Not Modified without a body. Clients should then reuse their cached payload.
When enabled via configuration, requests are limited per (Client IP, FeedLabel, FeedType) within a sliding time window (default 4 seconds if not configured). Exceeding the limit returns:
429 Too Many RequestsRetry-After: Whole seconds until the window resets.200 OK – Feed returned.304 Not Modified – Client cache still fresh.400 Bad Request – Missing or invalid FeedLabel/FeedType.404 Not Found – No current feed for the given combination.429 Too Many Requests – Rate limit exceeded.500 Internal Server Error – Unexpected processing failure.
Internally each feed tracks the originating GTFS-Realtime FeedHeader.timestamp. Even when no
underlying entity (trip, vehicle, or alert) changes, a periodic refresh ensures consumers know the data is
still valid as of the latest timestamp. Expiration logic (e.g., vehicle inactivity, alert aging, trip
suppression) occurs before serialization.
If-Modified-Since with the previous Last-Modified value.304, continue using previously parsed entities without re-download.Retry-After to avoid unnecessary throttling.Each message is atomic: consumers should treat the entity list as the authoritative snapshot for the requested feed type at the given timestamp.
using System.Net.Http;
using Google.Protobuf;
using TransitRealtime; // Namespace generated from gtfs-realtime.proto
var http = new HttpClient();
var bytes = await http.GetByteArrayAsync("https://host/GtfsProtoBuf?FeedLabel=regional&FeedType=TripUpdates");
var feed = FeedMessage.Parser.ParseFrom(bytes);
foreach (var entity in feed.Entity)
{
if (entity.TripUpdate != null)
{
Console.WriteLine($"{entity.Id} trip={entity.TripUpdate.Trip.TripId}");
}
}
curl -v -H "Accept: application/x-protobuf" \
"https://host/GtfsProtoBuf?FeedLabel=regional&FeedType=VehiclePositions" \
--output vehicle_positions.pb
<script src="https://cdn.jsdelivr.net/npm/protobufjs@7.2.4/dist/protobuf.min.js"></script>
<script>
(async () => {
const root = await protobuf.load('/assets/gtfs/gtfs-realtime.proto');
const FeedMessage = root.lookupType('transit_realtime.FeedMessage');
const resp = await fetch('/GtfsProtoBuf?FeedLabel=regional&FeedType=TripUpdates', {
headers: { 'Accept': 'application/x-protobuf' }
});
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const bytes = new Uint8Array(await resp.arrayBuffer());
const message = FeedMessage.decode(bytes);
console.log('Header timestamp:', message.header.timestamp?.toString());
console.log('Entity count:', message.entity.length);
message.entity.slice(0, 5).forEach(e => {
if (e.tripUpdate?.trip?.tripId)
console.log('TripUpdate entity', e.id, 'tripId', e.tripUpdate.trip.tripId);
});
})();
</script>
# Setup
npm init -y
npm install protobufjs
# Run
node fetch_gtfs.js
// fetch_gtfs.js
import protobuf from 'protobufjs';
const baseUrl = 'https://host/GtfsProtoBuf?FeedLabel=regional&FeedType=VehiclePositions';
let etag = null;
async function fetchOnce() {
const headers = { 'Accept': 'application/x-protobuf' };
if (etag) headers['If-None-Match'] = etag;
const resp = await fetch(baseUrl, { headers });
if (resp.status === 304) {
console.log('304 Not Modified - reuse cached payload');
return;
}
if (!resp.ok) {
console.error('HTTP error', resp.status);
return;
}
etag = resp.headers.get('etag');
const buf = new Uint8Array(await resp.arrayBuffer());
const root = await protobuf.load('gtfs-realtime.proto');
const FeedMessage = root.lookupType('transit_realtime.FeedMessage');
const msg = FeedMessage.decode(buf);
console.log('Timestamp:', msg.header.timestamp?.toString(), 'Entities:', msg.entity.length);
for (const e of msg.entity.slice(0, 3)) {
if (e.vehicle?.position)
console.log('Vehicle', e.id, e.vehicle.position.latitude, e.vehicle.position.longitude);
}
}
(async () => {
await fetchOnce();
await new Promise(r => setTimeout(r, 5000));
await fetchOnce();
})();
For production, pre-generate static JS classes with pbjs/pbts to avoid loading/parsing the .proto per run and leverage If-Modified-Since/If-None-Match.