Feed Publishing & Implemented Practices

  • Supported Realtime Feed Types

    The implementation provided by this service supports the GTFS-Realtime data exchange format with the following types of information:

    • Alerts – Unforeseen events affecting an agency and route.
    • Trip Updates – Updated arrival and departure information.
      • Includes trips whose arrival or departure times differ from scheduled values.
      • Trips with an associated vehicle, and other trips in the same block are included due to potential variance.
    • Vehicle Positions – Information about a vehicle's location.
      • Only vehicles logged into an active trip are included.
  • Protocol Buffer Specification

    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).

  • Feed Encoding & Access Method

    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}
  • Feed Refresh Cadence

    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.

  • Last-Modified & Conditional Retrieval

    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.

  • Protocol Buffer Feed Endpoint Details

    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.

    Request Pattern
    GET /GtfsProtoBuf?FeedLabel={feedKey}&FeedType={feedType}
    • FeedLabel: Case-insensitive key identifying the configured feed.
    • FeedType: One of Alerts, TripUpdates, VehiclePositions (case-insensitive).
    Example URLs
    /GtfsProtoBuf?FeedLabel=regional&FeedType=TripUpdates
    /GtfsProtoBuf?FeedLabel=regional&FeedType=VehiclePositions
    /GtfsProtoBuf?FeedLabel=agencyA&FeedType=Alerts
    Successful Response
    • 200 OK – Body is the serialized FeedMessage bytes.
    • Content-Type: application/x-protobuf
    • Last-Modified header reflects the feed header timestamp converted to RFC1123.
    Conditional Requests

    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.

    Rate Limiting

    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 Requests
    • Retry-After: Whole seconds until the window resets.
    Possible Status Codes
    • 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.
    Feed Freshness Strategy

    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.

  • Caching Guidance
    • Use If-Modified-Since with the previous Last-Modified value.
    • If 304, continue using previously parsed entities without re-download.
    • Respect Retry-After to avoid unnecessary throttling.
  • Data Integrity

    Each message is atomic: consumers should treat the entity list as the authoritative snapshot for the requested feed type at the given timestamp.

  • Consumption Examples
    C#
    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
    curl -v -H "Accept: application/x-protobuf" \
      "https://host/GtfsProtoBuf?FeedLabel=regional&FeedType=VehiclePositions" \
      --output vehicle_positions.pb
    Browser JavaScript (protobuf.js)
    <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>
    Node.js (ETag Conditional)
    # 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.