Two days ago I built a dispatch bot that parses my airport ride orders. Yesterday I added flight tracking — the bot fetches HKIA arrival data and matches it to my pickup orders. Today I needed push notifications.
Why push
When a flight lands, I need to drive to the airport. When it reaches the gate, I need to be in the parking lot ready to go. These are the two moments that decide my timing.
For landing alerts I could use VariFlight. It pushes notifications fine. But "at gate" is HKIA-specific data. And HKIA's own app is genuinely terrible. Hong Kong has no IT. So if I wanted at-gate alerts, I had to build them myself.
I already had the fetcher from yesterday. Today was about making it push to me instead of me pulling from the dashboard.
The architecture detour
The flight poller was sitting in web.py as a daemon thread. The Telegram bot was in bot.py. To send push notifications, the poller needs access to the bot instance.
I could bridge them. Shared queue, callback, event bus. But that's solving the wrong problem. The poller belongs in the bot process. Move it there. Done.
This wasn't a design decision. It was discipline. When something is in the wrong place, move it. Don't build infrastructure to work around it being in the wrong place.
The real problem: how often to poll
Push notifications are useless if they're late. I need to know a flight landed within a minute or two, not ten minutes later. The naive answer is to poll every minute.
But most of the time, that's wasteful. If a flight lands at 8pm and it's 2pm now, checking every minute for six hours is 360 requests that return the same data. Multiply by several orders in a day and it's hammering the API for nothing.
So I wanted the poller to check frequently when it matters and barely check when it doesn't.
Modeling how I actually check flights
I started thinking about what I'd do without the bot. When I accept a pickup order, the first thing I check is the ETA. If the flight lands in four hours, I don't think about it. I go do other things. Maybe take an Uber job, maybe grab food, maybe code in the parking lot.
As it gets closer, I start checking more. An hour out, I might look every fifteen minutes. Thirty minutes out, I'm glancing at it between tasks. Five minutes out, I'm refreshing constantly, because I need to be moving the moment it touches down.
And once the passenger is at the gate, I stop checking entirely. I'm already there.
That's the behavior I wanted to replicate. Start lazy, get more intense as landing approaches, stop when it's done.
The missing data problem
My first attempt was to model the full flight lifecycle. No active orders: don't poll at all. Order confirmed: check if the flight has departed. Not departed: sleep until estimated departure, then start tracking. Departed: poll based on ETA proximity. Landed: poll every minute until at gate. At gate: stop.
Clean model, but it hit a wall immediately. HKIA's arrival data doesn't include departure time. The plane takes off from another airport — HKIA only knows about the arrival side.
I could add another API. VariFlight or FlightAware would give me departure data. But now I'm adding a dependency to fill one gap in my model. And if I'm going to add APIs every time I hit a data gap, I might as well just poll every minute from the start and skip the complexity.
One formula instead of one dependency
I went back to my own behavior. What am I actually doing when I check more frequently as landing approaches? I'm looking at how far away the ETA is and deciding when to check next. The closer it gets, the sooner I check again.
That's just dividing the remaining time in half.
Flight lands in 4 hours → check again in 2 hours. Now it's 2 hours away → check in 1 hour. Then 30 minutes. Then 15. Then 7. Then 3. Then every minute.
max(60s, (ETA - now) / 2)
One line. No departure data needed. No extra API. The polling frequency naturally converges from hours-long gaps to per-minute checks right as the flight approaches landing. Exactly when I need the data to be fresh.
When there are no active pickup orders, the poller sleeps entirely. When a new order comes in, it wakes up and starts the cycle. When every flight is at gate, it goes back to sleep. The whole system breathes. Active when I need it, silent when I don't.
What I almost built vs what I actually built
I almost built a multi-source flight tracker with departure data, lifecycle state machine, and tiered polling rules. It would have worked. It would have been more "correct."
Instead I have one formula that mirrors how I'd check flights myself. It took ten minutes to implement and hasn't missed a notification yet.