Relaunch explorer: the app is killed during a send
A user taps Send. The app is killed while the message is in flight. The user opens the app again. Is the message sent once, twice or never? Change one setting and see the answer. This is a simplified model. Its numbers are my assumptions.
The idea
- A send is a few steps: the user taps, the app saves the message, the app sends a request, the server stores the message, the server replies.
- The system can stop the app at any step. A crash does the same.
- The outbox is a local table of messages that the server has not acknowledged. The app writes the row before it sends. A kill before the write loses the message. A kill after the write does not.
- The client message ID names the message. If the app saves it with the message and reuses it, a retry carries the same ID. If the app makes a new ID at retry time, the server sees a new message.
- Dedupe means the server remembers IDs. A request with a known ID gets the first result and stores nothing new.
- After a kill, the app must retry pending messages when it starts. If it does not, a saved message stays pending.
- Exactly once needs all of these together: save before you send, keep the ID, dedupe on the server and retry on launch.
This is a simplified model. Every number in it is my assumption, not a rule of any app or server. Real systems differ, so check yours. For the same idea with a lost reply and no kill, see the Retry explorer. The chat app case shows the design.
A suggested order:
- Run the first preset. A kill before the save loses the message.
- Run the second preset. The retry makes a new ID, so the server stores a second copy.
- Run the third preset. The same kill gives exactly one copy.
- Run the fourth preset. The same ID is not enough when the server does not check it.
- Then change one control at a time. Try turning off the outbox, or turning off retry on launch.
Try it
Result
Press "Run the simulation" or pick a preset.
Copies on the server is what the other person can see. The sender's screen is what the sender sees after the relaunch. The app shows only what it saved on disk.
Timeline
- Press "Run the simulation" or pick a preset.
What this shows
- Run the simulation to see what it shows.
Without JavaScript
The controls in Try it need JavaScript. If JavaScript is off, use this table. It shows what the four presets give. With JavaScript on, you can change every setting.
| Preset | What happens | Copies on the server | Sender's screen | Stuck pending |
|---|---|---|---|---|
| Kill before the outbox save | The app is killed after the tap and before the row is written. Nothing is saved, so nothing is sent after the relaunch. | 0 | Gone | No |
| Outbox but fresh ID on retry | The message is saved and sent, then the app is killed before the reply. On launch the app retries with a new ID. The server cannot match it. | 2 | Sent | No |
| Outbox, same ID, server dedupes | The same kill. The row kept the ID, so the retry carries it. The server recognises it and stores nothing new. | 1 | Sent | No |
| No dedupe on the server | The same kill and the same ID. The server does not check IDs, so the retry becomes a second copy. | 2 | Sent | No |
How the model works
The rules this tool uses. They are common practice, in a simplified form:
- The user taps Send. The app makes the message ID, shows a bubble and, if the outbox is on, writes a pending row. Then it sends the request.
- The server stores the message when the request arrives. If dedupe is on and it already knows the ID, it stores nothing new and answers with the first result.
- When the acknowledgement arrives, the app marks the message as sent.
- If the app is still running and the reply is lost, the request times out and the app sends the message again.
- If the app is killed, it comes back later. With the outbox on and retry on launch on, it sends every pending message again. With retry on launch off, a pending message stays pending.
- With the ID stored, the retry reads the same ID from the row. With a fresh ID, the retry makes a new one.
- With no outbox, the message exists only in memory. After a kill nothing is left to retry.
- After the relaunch the app shows what it saved on disk, plus what it receives in reply to its own sends.
My own choices for this tool, not from any standard:
- One network hop takes 0.3 s. A timeout is 10 s. After a timeout the app sends again once, at once.
- After a kill the user opens the app again 30 s later.
- A request that was sent before the kill has already reached the server and was stored. Only the reply is missing.
- The server remembers every ID it has seen. Real servers keep IDs for a limited time.
- The network works after the relaunch, and the reply to a retry arrives.
- There is one message, one user and one device. A kill happens once.
- The ID made at the tap is ID A. An ID made again at retry time is ID B.
- The outbox row is written in one step. It is either on disk or not.
What it leaves out: a history sync on launch that would bring a message back, matching server messages to pending rows by ID, the user typing the message again or tapping retry, several messages and their order, errors such as 4xx and 5xx, backoff, no network at launch, a server that stores the message and fails before it replies, and several devices.
Read more
- Chat app case: the send path, where a pending row keeps its clientId after a kill
- Retry explorer: what a lost reply does to a chat message
- Offline chat simulator: restart the app and turn on a new ID on retry
- Idempotency keys on mobile