Polling someone else's API politely: an interval that follows the vehicle's state, a per-user floor and one global token bucket The developer of ScooterLog, an independent app that logs charge and trip history for a connected electric scooter, built a roughly 180-line Python scheduler that decides when to poll the vehicle maker's upstream cloud API. The scheduler picks polling intervals from the last two stored readings (60 seconds riding, 180 charging, 300 parked, 1800 asleep), enforces a per-user minimum interval of 60 seconds, and caps all upstream traffic with a service-wide token bucket at 20 requests per second. The developer notes a fixed bug where silent upstream readings were mistaken for a parked vehicle, fixed by continuing to poll at the riding rate while the last reading is under 40 minutes old and shows the vehicle awake. Disclosure: I'm the developer of ScooterLog, and this post is published from my account. It was drafted and published by an AI agent working on my behalf from the app's source code. ScooterLog is an independent app, not affiliated with, endorsed by or sponsored by Ola Electric. ScooterLog https://play.google.com/store/apps/details?id=com.kkfe.scooterlog&utm source=devto&utm medium=article&utm campaign=2026-w45-polling keeps a charge history and a trip log for a connected electric scooter. There is no push feed to subscribe to, so the server polls the vehicle maker's cloud for each rider's scooter and stores what comes back. That cloud is not ours. Every request we send is a request someone else has to serve, and the fastest way to lose access to an upstream API is to look like a load test. This post is the scheduler that decides when to ask. It is about 180 lines of Python, and most of it is four ideas. A parked scooter at midnight changes nothing for hours. A scooter on the charger gains a percent every few minutes. A scooter being ridden changes position and odometer every minute. Polling all three at one fixed rate is either wasteful or too slow. So the next interval is chosen from the last two stored readings: RIDING = 60 seconds CHARGING = 180 PARKED = 300 ASLEEP = 1800 def next interval user id, vehicle id : pts = store.recent points user id, vehicle id, 2 if not pts: return PARKED last = pts 0 if last.get 'charging state' in 'SLOW CHARGING', 'FAST CHARGING' : return CHARGING if len pts == 2 and moved pts 0 , pts 1 or pts 0 .get 'odometer' or 0 - pts 1 .get 'odometer' or 0 = 0.5 : return RIDING ... return PARKED "Moved" is deliberately coarse: a change of more than 0.0001 degrees in latitude or longitude roughly 11 m , or half a kilometre on the odometer. GPS jitter on a parked vehicle should not flip it into riding mode. The first version had a blind spot. Mid-ride, the upstream often sends nothing new: the last reading stays the "awake" one from the start of the ride. With only the rules above, the scheduler saw two identical readings, decided the scooter was parked, and backed off to five minutes. The end of the ride was then caught up to five minutes late. The fix is one extra rule: if the last reading says the vehicle was awake, and it is less than 40 minutes old, keep polling at the riding rate. AWAKE MS = 40 60 1000 if not last.get 'sleep' and time.time 1000 - last.get 'source ms' or 0 < AWAKE MS: return RIDING The lesson generalises: when the source goes silent, "nothing changed" and "nothing was reported" look the same. Decide which one you are assuming, and bound how long you assume it. Whatever the rules say, no user is polled more often than MIN INTERVAL 60 seconds by default, set from an environment variable . The loop applies it after picking the fastest interval among that user's vehicles: due interval = min next interval u 'user id' , v 'id' for v in vs , default=PARKED due interval = max MIN INTERVAL, due interval if now - u 'last poll ms' or 0 = due interval 1000: poll user u 'user id' The floor is a separate line on purpose. The adaptive rules will change as we learn; the floor is the promise to the upstream, and it should not depend on those rules being right. Per-user floors do not stop a spike. If many riders start charging at 7 pm, many users become "due" in the same tick. So every upstream request also takes a token from a single, service-wide bucket 20 requests per second by default : python class TokenBucket: def init self, rate, burst=None : self.rate = rate self.capacity = burst or max 1.0, rate self.tokens = self.capacity self.ts = time.monotonic self.lock = threading.Lock def take self, timeout=30 : deadline = time.monotonic + timeout while True: with self.lock: now = time.monotonic self.tokens = min self.capacity, self.tokens + now - self.ts self.rate self.ts = now if self.tokens = 1: self.tokens -= 1 return True if time.monotonic = deadline: return False time.sleep 0.05 Two details matter. It uses time.monotonic , so a clock change on the server cannot mint a burst of tokens. And take gives up after a timeout and returns False ; the caller then skips that user for this tick instead of queueing forever. A skipped poll is picked up on the next pass. A queue that only grows is an outage in slow motion. Two smaller rules finish the picture. A 401 or 403 from upstream marks that user as needing to sign in again and stops polling them; retrying a rejected token on a timer helps nobody. And the main loop checks a kill switch before each pass: when it is on, the service makes no upstream calls at all and just sleeps. while not stop and stop.is set : if upstream.disabled : kill switch: no upstream calls at all time.sleep 30 continue ... Readings can lag by up to the current interval, so a charge alarm can arrive a few minutes after the battery reaches the level. The app says so: it only knows what the scooter reports, and readings can lag a few minutes. It does not control the scooter, and it is not meant for emergencies. If you poll an API you do not own, the transferable parts are: choose the interval from state, keep watching for a bounded time when the data goes quiet, put a fixed floor per user under the clever rules, and put one monotonic token bucket in front of everything. The app is on Google Play https://play.google.com/store/apps/details?id=com.kkfe.scooterlog&utm source=devto&utm medium=article&utm campaign=2026-w45-polling .