Skip to content
offline first for bad networks: what a delivery rider app taught us
article3 min read236 viewsSep 16, 2026

offline first for bad networks: what a delivery rider app taught us

Tunde Adebayo@tunde_ade

September 16, 2026 · 3 min read

#1 in Thumb Zone

236 views8 comments

last year we rebuilt the rider app for a logistics client (react native this time, the client already had a RN team). riders move between areas where network is good, then 2G, then nothing, then a filling station wifi that says "connected" but nothing loads. the old app treated network as always on and riders were losing deliveries, double submitting, and calling dispatch to confirm everything manually

here's what the second version does differently. not theory, this is what survived 6 months with real riders

writes go to a local queue first, always

every action the rider takes (picked up, arrived, delivered, photo, signature) is written to local storage first (we used SQLite) with status pending. the UI updates immediately from local data. a separate sync worker drains the queue to the server. the screen never waits on the network to say "done"

every mutation has an idempotency key

generated on the phone when the rider taps the button, stored with the queued item, sent as a header. if a request times out we dont know if the server got it, so we retry with the SAME key and the server returns the original result instead of creating a second delivery confirmation. before this we had riders "delivering" the same parcel 3 times on bad network. (yes @arjunmehta you were right, you can stop now)

dont trust "connected"

NetInfo will happily say connected on that filling station wifi. we use connectivity changes only as a hint to TRY syncing, and the real signal is whether a small request to our own health endpoint succeeds within a few seconds. isInternetReachable exists too, but we still do our own check

backoff with jitter

when a whole area comes back online every rider's app retries at the same second and the API falls over. exponential backoff (1s, 2s, 4s... capped at a couple of minutes) plus random jitter fixed it. classic, but we had to learn it the hard way

small payloads first, photos separate

status updates are tiny json and go first. proof of delivery photos are compressed on device (we resize to around 1280px, jpeg quality ~70, enough for a dispute) and upload separately in the background, resumable. a 3MB photo should never block "delivered" from reaching dispatch

show sync state honestly

each item has a small icon: saved on phone, sent, confirmed by server. riders actually look at it. it cut calls to dispatch a lot because they can see their stuff is waiting, not lost

conflicts: decide per field

for most fields last write wins is fine. for anything with money (cash on delivery amounts) the server is the authority and the app shows a conflict if they disagree. never let two offline phones silently overwrite money

if i was starting again i would design the sync queue on day one instead of adding it after launch. adding offline later means touching every screen. omo, it was a long quarter

Written by

Tunde Adebayo

Mobile dev (Flutter + React Native) at an agency in Lagos. building fintech and logistics apps mostly. if your app is slow on a 2GB android phone, its not "fine". football on weekends

Get the best of #thumb_zone weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

8

Sign in to join the conversation.

Sign in to comment
Arjun Mehta@arjunmehta#6 in Thumb Zone
Very happy to see idempotency keys in a mobile post. One small addition: the server must store the key with the response for at least as long as the phone may keep retrying. If the key expires before the last retry, you get the duplicate anyway.
1 net votes
Tunde Adebayo@tunde_ade#1 in Thumb Zone
we did 24h at first and yes, a rider who was offline for a whole weekend proved us wrong 😂 now its 7 days
0 net votes
Siobhan O'Rourke@siobhan_tests#6 in Thumb Zone
how do you test this? we tried simulating bad networks in CI and it was flaky as anything
2 net votes
Tunde Adebayo@tunde_ade#1 in Thumb Zone
honestly mostly manual plus one trick. emulator network throttling for "slow", airplane mode toggled with adb scripts, and a proxy that randomly drops the response AFTER the server already processed the request. that last one finds all the duplicate bugs. in CI we only test the queue logic with a fake transport, no real network
0 net votes
Marcus Johnson@marcus_oncall#4 in Thumb Zone
the jitter part. we had the exact same thundering herd on the backend when a whole region came back after an outage. y'all saved me writing this post
2 net votes
Mariana Costa@mari_costa#3 in Thumb Zone
The sync icon is such a good UX detail. Did you test the icons with riders? I always wonder if people understand the "cloud with arrow" metaphor.
2 net votes
Tunde Adebayo@tunde_ade#1 in Thumb Zone
we tested 3 versions with 8 riders. cloud icons confused them. what worked was a phone icon, then one tick, then two ticks. like whatsapp. everybody understands whatsapp ticks
2 net votes
Yuki Tanaka@yuki_ios#2 in Thumb Zone
Same approach work well on iOS with background URLSession for the photo upload, it continues even when the app is suspended. Thank you for the detail.
2 net votes