Each case on this site makes a few choices and says why. Every choice has a price. This page puts the same kind of choice side by side. You can see the options, what each one costs, and which case uses it.
I wrote the cases one at a time. This page is the view across all of them. Every row links to the section that backs it. That section says what I checked in official documents and what is my own opinion. The rows are not a full survey. They show what these cases chose, and other teams may choose differently.
Each table has four columns. "Pick it when" names the situation. "What it costs" names the price. "Where" links to the case sections. A choice that is right in one case can be wrong in another. Chat replays the messages that it missed. The live ticker takes a snapshot instead, because a new price replaces the old one.
How the app learns that data changed
In short: Polling asks again and again. A push tells the app to ask. A WebSocket keeps one line open while the app is on screen.
The question is how new data reaches the phone. The phone can ask, the server can tell it, or both can keep a connection open.
No option works in every state of the app, so the cases combine them. The chat app uses a WebSocket on screen and a push hint when the app is closed. The live ticker opens a WebSocket only for the rows the user can see.
| Choice | Pick it when | What it costs | Where |
|---|---|---|---|
| Polling (the app asks again and again) | You need the answer for a short time. Booking polls for about a minute after the bank page. | It uses battery and data. In the ticker, every answer is already old. | Booking app, 2, Chat app, 4.2, Live ticker, 2 |
| Push as a hint | The app is closed. The push only says that something changed, and a sync then fetches the data. | A push can be late or lost, so the sync must repair gaps. On iOS a silent push is low priority and not guaranteed. | Chat app, 4.2, Chat app, 4.3, Push SDK, 4.2 |
| WebSocket | The app is on screen and the set of items can change, as rows scroll. | You rebuild the connection after a drop. On iOS you do this yourself. In the background it is no use, because Doze stops network access. | Chat app, 4.2, Live ticker, 2, Live ticker, 4.3 |
| Server-sent events (a one-way stream over HTTP) | The set of items is fixed. This is simpler than a WebSocket. | The ticker needs subscribe and unsubscribe messages on the same connection, so it did not choose this. | Live ticker, 2 |
How to load a long list
In short: Page numbers break when the list changes. A cursor keeps the place.
Offset paging asks for "page 3". The server counts rows from the top, and the top moves. A new post makes an item repeat. A delete above makes an item go missing. A cursor is a saved place that the server gives to the app.
The short guide Cursor vs offset paging on mobile has the full table. You can watch both fail in the pagination race.
| Choice | Pick it when | What it costs | Where |
|---|---|---|---|
| Offset (page number) | The list is short and stable, and the user needs page numbers, as in an admin table. | A new item at the top repeats an item. A delete above skips one. | Cursor guide, side by side, Pagination library, 2 |
| Cursor (saved place) | The list changes while the user reads, as in feeds, chats, notifications and order lists. | You cannot jump to page 40. The server must keep a stable order and say what happens to an old cursor. | News feed, 2, Chat app, 2, Push SDK, 2 |
| Cursor and a repeat check by ID | The server may re-rank posts between two requests. | The app keeps one row for each post ID. I ask which part the backend does, and check anyway. | News feed, 2, Cursor guide, when I would choose which |
How to make a retry safe
In short: A retry is safe when the server can see that it is a repeat.
Here is the problem. The server did the work, the reply was lost, and the app sends again. An idempotency key is an ID that the app makes once and sends with every retry. The server saves the first result and returns it again.
The guide Idempotency keys on mobile explains who makes the key and how long it lives.
| Choice | Pick it when | What it costs | Where |
|---|---|---|---|
| A random UUID saved with the pending row | The action costs money or cannot be undone. It is the best choice in the guide. | The app must save it before the first send. The server must save the first result. | Booking app, 4.1, Chat app, 4.1, Analytics SDK, 2, Travel plans, 2 |
| No key, because the call sets a state | The action replaces a whole value, such as a like set to true. | The server must treat the call as a set. A toggle flips back on a retry. | News feed, 4.2, Idempotency guide, when I would choose which |
| A hash of the request body | I do not choose it. | It fails when two real actions are equal. | Idempotency guide, which key is safe |
| A new key on each try, a timestamp, or a counter | I do not choose these. | A new key makes each retry a new action. A timestamp can collide. A counter repeats after a reinstall. | Idempotency guide, which key is safe |
How to run work in the background
In short: Android keeps your job and runs it when its rules are true. iOS only gives you a chance to run.
The two systems make different promises, so one cannot replace the other. The guide WorkManager vs BGTaskScheduler compares them. This table starts from the job, as the guide does.
In every row, the result must not depend on the background run. The app catches up the next time the user opens it. That is why the table lists the job first and the tool second.
| Job | Android | iOS | Where |
|---|---|---|---|
| A message or order the user just made | Expedited work, with normal work as a fallback | Send it now. Keep it in your own saved list until the server confirms. | Chat app, 4.1, Chat app, 4.3 |
| A large upload the user started | A foreground worker, or a user-initiated data transfer job | A background URLSession, or BGContinuedProcessingTask on iOS 26 | Photo and file sync, 4.2 |
| A backup the user did not just ask for | Constrained WorkManager work | A background URLSession, with no promise about when it ends | Photo and file sync, 4.2 |
| Small updates to fresh content | Periodic work | BGAppRefreshTask. Treat it as a bonus. | Analytics SDK, 4.1, Background guide, when I would choose which |
| Heavy work, such as hashing | A worker with constraints | BGProcessingTask | Background guide, when I would choose which |
| A server that must wake the app | A high priority FCM message | A visible push, not a silent push | Chat app, 4.2, Push SDK, 4.3 |
Each tool has a limit. A periodic job on Android cannot repeat faster than every 15 minutes. While Doze is on, WorkManager tasks do not run. On iOS, a refresh task gets up to 30 seconds. A silent push has low priority. The guide cites the official pages for each limit.
How to settle a conflict
In short: Last write wins is simple. It is wrong when order, money or a pending change matters.
A conflict happens when two copies of the data change. The offline-first guide says to use last write wins only when losing the older write is acceptable and you can trust the clocks. You can also use it when the server sets the order. Otherwise, use a number or a version that the server owns.
| Rule | Pick it when | What it costs | Where |
|---|---|---|---|
| Last write wins | Losing the older write is acceptable and the clocks are trusted, or the server sets the order. | It fails when a clock is wrong. It also fails when two people change different fields: the older field disappears. | Offline-first guide, 4, Travel plans, 2 |
| A server number decides the order | The data has an order, as chat messages do. | The server must give each message a number. | Chat app, 2 |
| The local change wins until the server confirms | A pending change must not flip back, as a like must not. | The app needs a pending flag. A permanent rejection undoes the change. | News feed, 4.2 |
| A version check, then keep both | A file is edited from a base version. | The server answers 409, and the user gets a conflicted copy to resolve. | Photo and file sync, 6, Offline-first guide, 4 |
| A merge by field, with the server as the judge | Different fields should merge, and the same field should show a conflict. | Each change carries a base revision. The user sees a conflict with a keep-mine button. A delete leaves a tombstone. | Travel plans, 2, Travel plans, 4.2 |
| CRDT (a data type that merges edits by itself) | Two people type in one long text. | The history grows. Both platforms must follow its rules exactly. | Travel plans, 2, Travel plans, 6 |
| Refuse the write while offline | The action involves money or stock, as a booking does. | The user cannot book without a connection. | Booking app, 2 |
How to catch up after a gap
In short: After a gap, ask for changes after a saved place. If the gap is too long, take a fresh snapshot.
A snapshot is a copy of the data at one moment. The travel app takes one when the server says its changes are too old. The ticker takes one after a gap, because a new price replaces the old price. A chat message is different. Each message is a separate fact, so losing one loses information.
| Method | Pick it when | What it costs | Where |
|---|---|---|---|
| Ask for changes after a cursor | Each change is a fact that you cannot lose, such as a chat message. | The server must keep the changes. After a large gap, the app fetches the latest page and marks the gap. | Chat app, 4.2, Chat app, 5 |
| Ask for everything since the last sync time | I do not choose it. | It fails when the phone clock is wrong. Deletes never appear in the list. | Travel plans, 4.2 |
| Take a snapshot after a gap | A new value replaces the old one, as a price does. | The app ignores that symbol's ticks until the snapshot arrives. | Live ticker, 4.2 |
| Take a snapshot when the server says "too old" | The phone was away for weeks. | The app keeps its waiting changes and sends them again on top of the new data. | Travel plans, 4.2, Travel plans, 5 |
Where to go next
The other side of this page is the list of failure modes. It shows how an app fails and which sections handle each failure. If you are new, begin at Start here. Did you find a wrong row? Open an issue.
Written by Manuel Lorenzo. The site is drafted with AI assistance and edited and reviewed by him.