Mobile Design Rounds
Simulator

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:

  1. Run the first preset. A kill before the save loses the message.
  2. Run the second preset. The retry makes a new ID, so the server stores a second copy.
  3. Run the third preset. The same kill gives exactly one copy.
  4. Run the fourth preset. The same ID is not enough when the server does not check it.
  5. Then change one control at a time. Try turning off the outbox, or turning off retry on launch.

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.

PresetWhat happensCopies on the serverSender's screenStuck pending
Kill before the outbox saveThe app is killed after the tap and before the row is written. Nothing is saved, so nothing is sent after the relaunch.0GoneNo
Outbox but fresh ID on retryThe 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.2SentNo
Outbox, same ID, server dedupesThe same kill. The row kept the ID, so the retry carries it. The server recognises it and stores nothing new.1SentNo
No dedupe on the serverThe same kill and the same ID. The server does not check IDs, so the retry becomes a second copy.2SentNo

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