Realtime/SL

The static join and routeIdSource

The realtime feed almost never carries route_id — 2 in 1,000 vehicles. Joining trip_id against the static GTFS archive takes coverage to 96%, and marks every backfilled value.

GTFS-realtime identifies a trip by trip_id and, optionally, by route_id. SL's feeds almost never send the route id. Measured across this capture:

EntityRoute id from the feedAfter the static join
Vehicle positions 2 of 1,000  (0.2%) 964 of 1,000  (96.4%)
Trip updates 1 of 934  (0.1%) 934 of 934  (100%)

Without the join there is essentially nothing to group or filter by. ?routeId= would match two vehicles in the entire feed, list routes would be nearly empty, and every id in every response would stay an opaque sixteen-digit number.

How the join works

At load time the API reads three files from the static GTFS Sweden archive and builds lookup tables:

FileKeptGives
trips.txt 1,096 route_id, direction_id and trip_headsign for a trip id. Filtered to the trip ids the realtime feed actually references — this is a nationwide file, so keeping all of it would tie memory to the size of Sweden rather than to the size of the feed.
routes.txt 7,880 Short name, long name, agency and route type. Small enough to keep whole.
stops.txt 181,369 Stop names, coordinates, location type and parent station — the last of which also builds the station-to-platform map.

routeIdSource

Enrichment is additive and never silent. When a routeId was backfilled from trips.txt rather than supplied by the feed, the trip reference is marked:

a backfilled trip reference
{
  "trip": {
    "tripId": "14010000710000247",
    "routeId": "9011001087500000",
    "routeIdSource": "static",
    "directionId": 1,
    "startDate": "20260427",
    "route": {
      "routeId": "9011001087500000",
      "agencyId": "505000000000000001",
      "shortName": "875",
      "type": 700
    }
  }
}

routeIdSource: "static" means “we looked this up”. Its absence means the feed sent the route id itself — which, in this capture, happens three times in total. If you are building anything that reasons about data provenance, that one key is the difference between a reported fact and a joined one.

When there is no match

A trip id absent from the archive is left exactly as the feed reported it. No route object, no headsign, no invented name — the fields are simply not there, in keeping with absent is not zero. That is why vehicle coverage is 96.4% rather than 100%: those trips have no counterpart in the archive, usually because the static feed and the realtime feed were published at different times.

You can see the join's health directly at /api/feed, under static. If available is false, the archive was not found: the API still serves realtime data, but unenriched — no names anywhere, and route coverage back down to under 1%.

GET /api/feed → data.static
{
  "available": true,
  "dir": "/…/data/gtfs",
  "counts": {
    "routes": 7880,
    "stops": 181369,
    "trips": 1096,
    "tripsRequested": 1096
  },
  "loadMs": 1162
}

tripsRequested is how many distinct trip ids the realtime feed referenced; trips is how many of those were found. A gap between them is the unmatched remainder — worth watching if enrichment starts thinning out.