You're on a train, signal drops to zero. You type a message and hit send. A single grey tick appears.
Then — the moment you exit the tunnel — two ticks appear.
How did that work? You were offline.
This isn't magic. It's offline-first architecture, and it's one of the most practical ideas in mobile engineering. Let's break it down.
The Scenario: Sending a Message in Airplane Mode
Imagine you toggle airplane mode and type:
"Just took off. Landing in 3 hours!"
WhatsApp doesn't show an error. It doesn't freeze. You see a message bubble with one grey tick.
Here's what actually happened:
- The app saved your message to a local database on your phone
- It added the message to an outgoing queue
- It rendered the message immediately in the UI — no waiting
- It set the internal status to PENDING
Your message never touched a server. The app made a promise to deliver it when connectivity returns. That promise — that optimistic local-first action — is the heart of offline-first design.
Local Storage and Message Persistence
Every message is first written to a local SQLite database on the device, before anything goes to the cloud.
WhatsApp uses SQLite. Android apps use Room. iOS apps use Core Data. In React Native, you'd reach for expo-sqlite or @op-engineering/op-sqlite.
A simplified message record looks like this:
CREATE TABLE messages (
id TEXT PRIMARY KEY, -- Generated on device, not server
conversation_id TEXT NOT NULL,
content TEXT NOT NULL,
created_at INTEGER NOT NULL, -- Device timestamp
status TEXT DEFAULT 'PENDING',
media_uri TEXT, -- Local file path for media
server_ack_id TEXT -- Set after server confirms receipt
);
Two things worth noting:
- The ID is generated on the device, so WhatsApp can display the message before the server even knows it exists.
- Status starts as PENDING — honest, explicit, and essential for the queue system.
The UI reads from local storage. It never waits for the network.
The Message Queue
When a message is saved locally, it also enters an outgoing queue — a persistent list of things the app needs to send when it can.
┌──────────────────────────────────────┐
│ OUTGOING MESSAGE QUEUE │
│ [msg_001] "Just took off..." PENDING│
│ [msg_002] "See you soon!" PENDING │
│ [msg_003] photo.jpg PENDING │
└──────────────────────────────────────┘
↕ No internet — held on device
This queue is written to disk, not held in RAM. If the app crashes, the queue survives. When internet returns, the app processes messages in order, retrying failures with exponential backoff (1s → 2s → 4s → ...) to handle flaky connections gracefully.
Syncing When Connectivity Returns
The moment internet returns, the app fires off four steps almost simultaneously:
Internet returns
│
▼
1. Flush outgoing queue → Send all pending messages to server
│
▼
2. Pull missed messages → Fetch messages sent to YOU while offline
│
▼
3. Reconcile local DB → Merge server state with local state
│
▼
4. Update the UI → Ticks change, new messages appear
Step 1 — Each queued message is sent to WhatsApp's servers. The server acknowledges receipt and the status upgrades from PENDING to SENT.
Step 2 — People may have messaged you while you were offline. The app requests everything from the server since the last sync timestamp.
Step 3 — Local DB is updated with what came from the server.
Step 4 — Because the UI observes the local DB reactively, everything updates automatically. No manual refresh needed.
Delivery States: Sent, Delivered, Read
The tick system maps directly to a message state machine:
[PENDING] → [SENT] → [DELIVERED] → [READ]
clock 1 grey 2 grey 2 blue
tick ticks ticks
└──────→ [FAILED] (retries exhausted)
| State | What it means |
|---|---|
| Pending | Saved locally, not yet on server |
| Sent | Server received it |
| Delivered | Recipient's device has it |
| Read | Recipient opened the conversation |
| Failed | Couldn't deliver after all retries |
State transitions are triggered by acknowledgements (acks) from the server or recipient's device via a persistent connection.
Why separate "Sent" from "Delivered"? Because the server having your message ≠ the recipient's phone having it. They could be offline too.
Handling Media While Offline
Text messages are tiny. Photos and videos are not. WhatsApp handles this with a decoupled upload strategy:
- Capture & save locally — photo is stored on device
-
Queue the message with a local file reference (
local://img_001.jpg) - Upload media in background when internet returns
-
Server returns a CDN URL — message record updates from
local://tohttps://mmg.whatsapp.net/... - Recipient downloads on demand — they get the URL and fetch media lazily
This is why you sometimes see a blurred thumbnail or "Waiting for media" — the text message arrived, but media hasn't been downloaded yet.
Conflict Resolution and Message Ordering
What happens when messages arrive out of order after going offline?
The problem: Device clocks are unreliable. Your phone might be 30 seconds off, or in the wrong timezone. If WhatsApp trusted device timestamps alone, messages could appear out of order.
The solution:
- Server-assigned timestamps — when a message reaches the server, it gets a canonical timestamp used for ordering
- Sequence numbers — monotonically increasing numbers break ties between messages with identical timestamps
- Last-write-wins for status — message status can only move forward (SENT → DELIVERED, never backwards)
This is a simplified form of eventual consistency:
The system doesn't guarantee every device sees the same state at the same instant. But given time and connectivity, all devices converge on the same state.
For messaging, this is an acceptable tradeoff. A message appearing 200ms out of order is fine. A message that never appears is not.
Why Offline-First Matters
Traditional approach:
User action → Network request → Update UI (only if network works)
Offline-first approach:
User action → Update local DB → Update UI → Background sync to server
The difference in feel is enormous.
| Traditional (online-first) | Offline-first |
|---|---|
| Broken without internet | Works fully offline |
| User waits on every action | Instant UI feedback |
| Lost actions on failure | Queue retries automatically |
| Spinner on every tap | Smooth always |
| Terrible on slow networks | Great on any network |
The tradeoffs are real though:
- Conflict resolution becomes your responsibility
- Local data needs encryption (WhatsApp uses SQLCipher)
- Testing network edge cases is harder
- Storage management (when do you purge old queued messages?)
The Full Lifecycle in One Diagram
User types and hits send
↓
App generates local ID
↓
Saved to SQLite (PENDING)
↓
Added to outgoing queue
↓
UI renders immediately (optimistic update)
↓
─── [Internet returns] ───────────────────
↓
Queue flushed → sent to server
↓
Server acks → status: SENT (1 grey tick ✓)
↓
Recipient's device comes online → delivered
↓
Delivery ack → status: DELIVERED (2 grey ✓✓)
↓
Recipient opens chat → read receipt sent
↓
Read ack → status: READ (2 blue ✓✓) ✅
Building This in React Native
Here's the toolkit you'd reach for:
| Need | Library |
|---|---|
| Local DB |
expo-sqlite or @op-engineering/op-sqlite
|
| Fast key-value (queue) | react-native-mmkv |
| Network state | @react-native-community/netinfo |
| Background sync |
expo-task-manager + background fetch |
| Real-time acks |
socket.io-client or Firebase RTDB |
The pattern is always: local first → background sync → UI driven by local state.
Key Takeaways
- WhatsApp never waits for the network — it writes locally and syncs later
- The outgoing queue persists to disk and survives crashes
- Delivery states (PENDING → SENT → DELIVERED → READ) are driven by server and device acks
- Media uploads are decoupled from text — text queues fast, media uploads in background
- Server-side timestamps and sequence numbers resolve ordering conflicts
- Offline-first trades implementation complexity for much better UX
What to Explore Next
- WatermelonDB — a React Native DB built specifically for offline-first at scale
- CRDTs — the math behind conflict-free sync
- Firebase Firestore offline persistence — a managed offline-first solution
- XMPP / MQTT — the protocols behind real-time messaging
Next time you see those grey ticks turn blue on a plane with spotty Wi-Fi, you'll know exactly what just happened. 🛫
Found this useful? Drop a ❤️ and share it with your dev cohort!
Top comments (0)