Delivery and store-and-forward

Delivered, accepted, committed: what does "delivered" mean?

A message is delivered when it reaches the receiving device or server. That is not the same as the receiving application accepting it, or the result being durably committed. The three are separate outcomes that can succeed or fail independently, so a reliable app tracks each one and treats the work as done only when the confirmation it needs arrives.

Learning objectives

After reading this article you will be able to:

  • Distinguish delivered, accepted and committed as outcomes that can fail independently
  • Explain what email delivery reports and MQTT acknowledgments actually confirm
  • Describe how an offline app should track and show each delivery state

Three confirmations, not one

When a messaging system says a message was “delivered”, it is reporting one specific event, and which event depends on the system. Three are worth keeping apart:

  • Delivered. The bytes reached the receiving device or server. A transport acknowledgment says this much and no more.
  • Accepted. The receiving application checked the message and took responsibility for it: it parsed it, validated it and decided to act on it.
  • Committed. The result was written somewhere durable, so a crash or restart will not lose it. In many systems the commit that matters most happens later, at a backend.

Each step can succeed while the next one fails. A message can reach a phone whose app crashes before storing it. An app can accept a task and then lose it because it confirmed before writing to disk. A device can record a handoff that the backend later rejects. Treating any one of these as the whole story is how work gets lost without anyone noticing.

What standards mean by delivered

Protocols that report delivery define it narrowly, and the definitions are worth reading.

Email delivery status notifications (DSNs, RFC 3461) say a message is “delivered” when it has been placed in the recipient’s mailbox. For mail collected later over IMAP or POP, delivery happens when the message is made available to that service, not when the recipient’s mail program fetches it. When a mail server passes a message to a system that cannot report back, the most it can send is a “relayed” notice.

Message disposition notifications (MDNs, RFC 8098) report what happened at the recipient’s mail program. Even the “displayed” disposition comes with a caveat in the standard: there is no guarantee that the content has been read or understood. The RFC also recommends asking the user before sending one, with the default set to not sending, which is why read receipts are so often missing.

MQTT 5.0 shows the same gap inside a broker. For a QoS 1 publish, the receiver answers with a PUBACK once it has accepted ownership of the message, and the specification says it does not need to complete onward delivery first. One PUBACK reason code, “No matching subscribers”, means the message was accepted but nobody was subscribed to it.

An acknowledgment is only as strong as the event its protocol defines.

Accepted: only the application can say it

In “End-to-End Arguments in System Design”, published in 1984, Saltzer, Reed and Clark use delivery acknowledgment as one of their oldest examples. The ARPANET sent a packet back to the sender whenever it delivered a message to a host, and the authors note that this was never found to be very helpful to applications. What an application wants to know is whether the target acted on the message, because, in their words, “all manner of disaster might have struck after message delivery”. The acknowledgment that is really wanted can only come from the target application: “I did it”, or “I didn’t.”

That is the acceptance step. The receiving application, not the network, decides whether a message is valid, authorised and new. It must also be able to say no: a rejected message was still delivered, and the sender needs to hear the rejection rather than assume success. Because a sender that hears nothing will retry, acceptance has to be idempotent, so a repeated message returns the stored result instead of doing the work twice.

Committed: durable, and durable where

A commit is a promise that the result survives a crash. Even that word needs care. PostgreSQL normally waits for a transaction’s write-ahead log records to reach permanent storage before it reports success. With asynchronous commit, it reports success as soon as the transaction is logically complete, and its documentation warns that the most recent transactions may then be lost if the server crashes. Its advice is not to use that mode when the client will take external actions that rely on the transaction being remembered.

The same rule applies to an app sending a confirmation: write the result durably, then acknowledge. A confirmation sent before the write promises something the device cannot yet keep.

In connected systems a commit is often the only step anyone tracks. Offline systems usually have two: a local commit on the device that accepted the work, and a backend commit later, when that device or another one reaches the server. The backend can still reject what the device accepted, for example because another device claimed the same item first.

Why offline apps need all three

Online, the three outcomes often arrive within a second of each other, so collapsing them rarely shows. Offline, they can be hours or days apart. A message can wait in an outbox, reach a nearby phone over Bluetooth, be accepted there, and then wait again until someone walks back into coverage before the backend sees it.

Offline Protocol’s local handoff guide is one documented example of keeping them apart. Its SDK reports message_delivered when a message reaches the other device, and the guide notes that the receiving app may not have stored the record yet. The receiving app commits the operation in one local transaction and only then sends an application receipt, which the sender treats as acceptance. Backend commit is a separate state, and a backend rejection stays visible instead of silently rewriting the local history.

Show users the state you actually have

The practical rule is to give each outcome its own state and its own indicator:

StateWhat it proves
SavedStored on this device, waiting to send
DeliveredReached the other device or server
AcceptedThe receiving application took responsibility
CommittedDurably recorded where it needs to be
Expired or rejectedSomeone needs to act

Keep pending work visible while the device is offline, and give every failure a recovery action. A tick that means “delivered” will be read as “done”, so use the strongest wording only when you hold the strongest evidence.

Frequently asked questions

Is a read receipt the same as a delivery receipt?

No. A delivery report says a message reached a mailbox or device. A read receipt reports that it was shown to someone, and even the email standard for read receipts says that is no guarantee the content was read or understood. Recipients can also turn read receipts off.

Which confirmation should my app wait for?

The one that matches the promise you make to the user. For a chat message, delivery to the other device may be enough. For a task handoff, a payment or an inspection record, wait for the receiving application's acceptance, and track the backend commit separately.

Sources

Build it with Offline Protocol

The Offline Protocol local handoff guide lists pending, delivered, accepted locally and committed upstream as separate states, with the evidence for each and the receipt the receiving app sends after its local commit.

Read the local handoff guide