This is doable, but not through the API alone. Live Activities have no timeline, keeping the app alive to implement a "timeline" is not a good solution as you also realize, and one push per arrival won't scale.
The way through is to stop treating the push as the display and treat it as a data feed.
The numbers. From what I can recall based on the MTA Developer site, the system has about 470 stations and roughly 500 trains in service at peak. But you will surely have more accurate numbers.
A train reaches a new station every couple of minutes, so system-wide that's on the order of 15,000+ arrivals an hour. One push per arrival is a non-starter. But the shortest run between two stations is about 90 seconds (you probably have more accurate info, correct?). So if you send a snapshot every 90 seconds, no train can pass more than one station between updates, and the device never loses track of where a train is. Each train's state also packs into about 3 bytes, so the whole system fits in about 1.5 KB of raw data, or about 2 KB after encoding. That's well inside the payload limit.
The view does the work, not the push. Each update is rendered by your own SwiftUI code in the widget extension, so the view can decode a compact payload and compute arrivals for the user's station. Timer-based text and progress views count down on their own between updates, and the view can drop trains whose arrival time had already passed when it rendered. A train that arrives between pushes simply sits at "arriving" until the next update. One risk to consider would be when the system decides to re-render the Live Activity. whether rendering happens as each push arrives, or later when the Lock Screen or Dynamic Island is shown will be up to the device state.
Push a snapshot of the whole system on a fixed cadence. Use one broadcast channel and send a full system snapshot every 90 seconds or so, regardless of how many trains arrived. Because each push is a complete snapshot rather than a change list, a push lost in a dead zone only costs staleness, and the next one corrects everything. A train that has just arrived sits at "arriving" until the next push, which is accurate enough to read as "at the platform."
Encode trains, not stations. Encoding arrivals per station repeats every train at every downstream stop and won't fit the payload limit. Instead, encode each train's position, and let the device work out arrivals at its own station. Each train becomes a fixed 3-byte (24-bit) record: which stopping pattern it is running (9 bits, which also implies route and direction), the index of the next stop on that pattern (7 bits), and how long until it reaches that stop, in 5-second units (8 bits, up to about 21 minutes). Size these field widths from your actual data of course, not from my guesses. Concatenate all the records and base64-encode the result into a single string field in the content state, alongside a snapshot timestamp and a version number for the schedule data:
"content-state": { "v": 42, "t": 1760375575, "d": "AIEXAgc5…" }
Three bytes map to exactly four base64 characters, so 500 trains become a 2,000-character string. Base64 characters need no escaping in JSON. A JSON array of numbers holding the same data would be roughly three times larger.
On the device, the view decodes the string back into records. For each train on a pattern that serves the user's platform, it computes the arrival as: snapshot time + time to the train's next stop + scheduled run time from that stop to the user's station. Those run times would come from a table bundled with the app. If the version number in the payload doesn't match the bundled table, the view should say so rather than show wrong times.
The hard part is the bundled schedule data. That data has to stay in sync with the server, including weekend and overnight service changes. Version it, and decide what the view shows when it meets a train pattern it doesn't recognize. Those kinds of updates can probably be left for when the app is launched by the user to start the activity anyways.
Most of the real effort is in those last details: the record layout, the schedule data pipeline, and handling service changes. As they say "Some engineering might be required".
Happy to compare notes on any of it if useful.