Every so often we get a message that says the arrival time in BusanBus was wrong. The bus said four minutes and took seven. Or it said three minutes and arrived immediately. The person is not mistaken and the app is not broken, and explaining what is actually happening turns out to be more useful than apologising.
Where the number comes from
BusanBus does not observe buses. It reads a public transit feed, which reads a system that receives position reports from vehicles.
A bus reports its position on an interval. That report travels to the transit authority's system, which matches it to a route, works out how many stops remain to each downstream stop, and produces an estimate of minutes to arrival. That estimate is published through an open data endpoint. We request it. The response travels back over a mobile network to a phone in someone's hand.
By the time a number renders on screen, it has been through several hops, each with its own latency, and it is derived from a vehicle position that was current some seconds or tens of seconds ago. The arrival feed for a stop typically gives you two upcoming vehicles, each with an estimated minutes value and a remaining stop count.
The stop count is the more honest of the two. Minutes are a model output — the system's guess about how long those remaining stops will take, which depends on traffic, signals, and how many people board. Remaining stops is closer to a measurement. When the two disagree in a way that matters, we show both, because "three stops away" is information a rider can act on even when the minutes figure is moving around.
The feed fails in specific ways, and they are not all failures
Building against a public data endpoint teaches you that HTTP 200 does not mean success.
Korean public data services commonly return an envelope with a result code inside the body. An expired key, an exceeded daily quota, or a temporarily unavailable backing service all arrive as a well-formed response with a non-success code and no items. Treating that as "no buses are coming" is the bug that produces the worst possible user experience: a stop that appears to have no service.
There is also the service key encoding problem, which nearly everyone who has built against these APIs has hit. The portal issues both an encoded and a decoded form of the key, and depending on how your HTTP client handles query parameters, you can end up double-encoding it. The result is an authentication failure that looks nothing like an authentication failure. We pin the form we send and assert on it, because this cost us an afternoon once and we do not intend to spend another.
Then there are the real degradations:
A stop with no reporting vehicles. Late at night, or on a low-frequency route, there genuinely is no next bus in the window the feed covers. This is different from an error and must look different.
A vehicle that has stopped reporting. Its last known position sticks, and the estimate ages. Some feeds keep publishing a stale estimate rather than withdrawing it.
Service that has ended for the day. First and last bus times are static data, and they are the correct answer to "when is the next bus" at 1 a.m., not an empty list.
BusanBus distinguishes these. An empty arrival list at midnight shows the first bus tomorrow. An empty list at 8 a.m. on a busy route says the feed returned nothing and offers a retry, because that is almost certainly upstream trouble rather than a route that stopped existing.
Why we do not poll faster
The most common feature request we decline is a faster automatic refresh.
Polling every two seconds does not make the data fresher. The upstream estimate updates on its own cadence, tied to how often vehicles report. Requesting more often than that gets you the same number back, repeatedly, while consuming the user's battery and mobile data and burning through a quota that is shared across every user of the app.
What we do instead:
Refresh on the interval the data actually changes on, and stop when the screen is not visible. A stop detail screen left open in a pocket should not be polling.
Show the age of the data. A number without a timestamp invites you to believe it is current. A number with "updated 20 seconds ago" underneath it tells you how much to trust it, and it makes a frozen feed obvious rather than invisible.
Cache the last successful response and show it, labelled, when a request fails. Standing at a stop with poor reception, a two-minute-old estimate marked as two minutes old is much more useful than an error screen. The label is not optional — an old number presented as current is worse than nothing.
Back off on repeated failure rather than hammering. If the upstream is having a bad afternoon, thousands of clients retrying aggressively is part of the problem.
Stops, directions, and the mistake everyone makes
The other half of the app is finding the right stop, and the failure mode here is the most consequential one in transit software: right route number, wrong side of the road.
Bus stops in Busan carry an ARS number, a short identifier printed on the physical stop sign. This is the only unambiguous handle. Names are not unique — a major intersection can have several stops sharing a name, differing only by direction and by which corner they are on. Searching a name and picking the first result is how you end up travelling confidently in the wrong direction.
So the app leans on three things when names collide: the ARS number, the direction the route is travelling at that stop, and the map. Search results that share a name are shown with direction and location rather than being deduplicated into one row, because collapsing them hides exactly the distinction you need.
The single most useful habit is to check the ARS number on the physical sign against the one in the app the first time you use a stop. It takes five seconds once, and it settles the question permanently.
Setting it up so a check is one tap
The setup that works is small.
Save the stop, not the route. Routes are how the network is organised. Stops are where you stand. A favourite named for a route requires you to re-choose a direction every time.
Save both directions separately. The stop you use in the morning and the one you use going home are two different stops with two different ARS numbers, even though the sign says the same thing. Save them as two favourites.
Name favourites for the trip, not the place. "Home to Seomyeon" is legible at 7 a.m. in a way that a stop name is not. You are choosing between your own saved items, so the name should describe your intent.
Add a home screen shortcut for the one you check daily. The whole value of a focused transit app is skipping the search, and a shortcut skips opening the app.
Look up first and last bus times before you need them. These are static and reliable, and they are the piece of information that actually determines whether a late trip is possible. Real-time arrivals cannot tell you anything useful at 12:40 a.m.; the last bus time can.
What we are not going to promise
BusanBus cannot make the upstream feed more accurate. If a vehicle is not reporting, we do not know where it is, and no amount of interface polish changes that. What the app can do is be clear about what it knows, how old that knowledge is, and when it is guessing.
We would rather show "no recent data for this vehicle" than a confident-looking number that is fifteen minutes stale. In the field, riders adjust well to uncertainty when it is labelled and badly when it is hidden.