Before we get overwhelmed by phrases like "eventual consistency" and "offline-first architecture," let's just talk about something you've probably done a hundred times. You're on a train through a tunnel, the signal drops to zero, and you send a message anyway. WhatsApp shows a single grey tick almost immediately. The message didn't go anywhere — there's no internet. So what just happened?
That single tick is doing a lot of quiet engineering work. And understanding it unlocks how a huge category of modern apps think about reliability, state, and user trust.
This is not as mysterious as it sounds. Let's walk through it.
The Moment You Hit Send With No Internet
Picture this: you're on a flight, airplane mode is on, and you type "landed safely, will call soon" and tap send. WhatsApp shows the message instantly in your chat — a single grey tick. The app didn't freeze. It didn't say "no network, try again." It just... worked.
The obvious question here is: where did that message actually go?
It went nowhere yet. It went into your phone's local storage — a small database sitting right on your device. WhatsApp writes your message to this local store first, marks it as "pending," and shows it to you immediately. The network hasn't been involved at all.
This is the core idea behind offline-first design: the app treats your local device as the source of truth, not the server. You write locally, you sync later.
Sounds simple. The engineering to make it reliable is not.
Your Phone Is a Mini Post Office
Think of your phone as a local post office in a small town cut off by floods. You walk in, drop a letter, and get a receipt (the grey tick). The letter sits in the post office's outbox. The moment the roads reopen, the truck leaves and delivers everything.
The post office doesn't tell you, "Sorry, roads are closed, come back later." It accepts your letter, stores it, and handles delivery when it can. That's exactly what the message queue on your device does.
Every message you send while offline enters this queue — an ordered list of pending actions your app will retry as soon as connectivity returns. The queue persists even if you close the app, because it's written to local storage (usually SQLite on mobile), not just held in memory.
What "Local Storage" Actually Means Here
WhatsApp (and apps like it) keep a local copy of your entire recent conversation history on your device. This isn't just a cache for speed — it's a deliberate design decision, so the app can work without a server.
When you open a chat, you're reading from this local database. When you type and send, you're writing to it. The server sync happens in the background, asynchronously.
The local database tracks a few critical things for each message:
- The message content
- A unique ID generated on your device
- A timestamp from the moment you wrote it
- A delivery status:
pending,sent,delivered,read
That device-generated ID is more important than it looks. It's what lets the server recognize your message as unique even if it receives it multiple times (say, after a network retry). No duplicates, no confusion.
The Queue Comes Alive When You Reconnect
The moment your phone gets a signal back — WiFi, 4G, doesn't matter — the app's sync process kicks in. It reads through the pending queue, oldest first, and starts pushing messages to the server.
Now wait a minute — what if two messages were queued up and they arrive at the server out of order?
Good catch. This is why the device-generated timestamp matters. The server uses it to reconstruct the correct message order, even if network delivery got scrambled. The queue on your device sends in order; the timestamp is a fallback guarantee.
Once the server receives a message and stores it, it sends back an acknowledgement. Your app updates that message's status to sent — and the grey tick becomes a double grey tick.
The Three Ticks: What Each State Actually Means
This is where offline messaging connects to something you see every single day.
Single grey tick — Sent. Your device has written the message to the server. That's it. The recipient hasn't been involved yet. If they're offline, it just means the server is holding it for them.
Double grey tick — Delivered. The server pushed the message to the recipient's device, and their WhatsApp app acknowledged receipt. The message now lives in their local database too. The recipient may not have opened it — delivered just means their phone got it.
Double blue tick — Read: The recipient opened the conversation. Their app sent a read receipt back to the server, which relayed it to you. Blue ticks require the recipient to actively open the chat.
Each state is a separate network event. Each can be delayed independently. That's why you sometimes see a message go from single tick to double blue all at once — the recipient was offline when it was delivered, then opened it later, and both events reached you simultaneously.
Media Is a Separate Problem
A text message is a few hundred bytes. A video can be 50MB. Offline handling for media is fundamentally different.
When you send a photo while offline, the app queues an upload intent — not the photo itself. The local database notes: "When we're back online, upload this file from this local path, then send the message that references it."
When connectivity returns, the media uploads first to WhatsApp's servers. Once the upload completes and the server gives back a URL, the text message referencing that URL is sent. Only then does the recipient see the media.
Sounds like a massive headache, right? But this separation is intentional. If both the upload and the send were one atomic operation, a failed upload midway would mean starting over completely. By splitting them, the app can resume an interrupted upload from where it left off — not from scratch.
The Real Engineering Part
Conflict Resolution and Message Ordering
Here's where it gets genuinely tricky. Imagine you and a friend both go offline at the same time and both send messages to a group chat. When you both reconnect, whose messages get sent first? What order do they appear in?
The short answer is: the server decides, and it uses timestamps to do it. But timestamps from different devices are never perfectly synchronized. Two phones with a 2-second clock drift can disagree on which message came first.
Most messaging systems solve this with a technique called logical ordering — the server assigns a monotonically increasing sequence number to each message as it arrives. This becomes the "official" order, regardless of what the device clocks said. Your local UI then reorders messages to match the server's sequence.
This creates an interesting moment you may have noticed: you send a message, it shows at the bottom of your chat, then jumps slightly upward after syncing. That's message reordering happening in real time.
Eventual Consistency at the User Level
Eventual consistency is a scary-sounding term from distributed systems. In plain English, it means: not everyone will see the same state at the same moment, but eventually, everyone will agree.
WhatsApp is eventually consistent. If you send a message to a group, some members might get it in 2 seconds, others in 30 seconds. For a moment, the group chat looks different on different phones. But within some window of time — usually seconds — everyone catches up.
The tradeoff being made here is availability over perfect real-time accuracy. The app prioritizes letting you send and receive messages even under bad conditions, accepting that momentary inconsistency is less annoying than failure.
The Catch: When Offline-First Gets Complicated
The edge cases are where this gets interesting.
Message expiry is one. If your message sits in the pending queue for too long — say, you're offline for 30 days — the server may no longer accept it. The session might have expired, encryption keys may have rotated (WhatsApp uses ephemeral keys via the Signal Protocol), and the queued message simply fails silently or returns an error your app has to handle gracefully.
Another edge case: editing and deletion. When you delete a message "for everyone," that's a new queued action — a delete event with the target message ID. If you're offline when you tap delete, the delete event queues up. But if the recipient comes online before you do and reads the message before your delete event reaches the server, the delete may still succeed, but they've already seen it. Eventually consistent can mean "eventually deleted, but too late."
Clock manipulation is another real issue. If a user manually sets their phone clock backwards, device-generated timestamps can look like messages from the past. Servers defend against this with server-side timestamps as authoritative when there's a large discrepancy.
DIY: Build It Yourself
Here's a minimal offline-first message queue in TypeScript — the core pattern without the complexity of a full chat app.
import AsyncStorage from '@react-native-async-storage/async-storage';
interface QueuedMessage {
id: string;
content: string;
recipientId: string;
timestamp: number;
status: 'pending' | 'sent' | 'failed';
}
async function queueMessage(content: string, recipientId: string): Promise<void> {
const message: QueuedMessage = {
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
content,
recipientId,
timestamp: Date.now(),
status: 'pending',
};
const raw = await AsyncStorage.getItem('pendingMessages');
const queue: QueuedMessage[] = raw ? JSON.parse(raw) : [];
queue.push(message);
await AsyncStorage.setItem('pendingMessages', JSON.stringify(queue));
}
async function flushQueue(sendToServer: (msg: QueuedMessage) => Promise<boolean>): Promise<void> {
const raw = await AsyncStorage.getItem('pendingMessages');
if (!raw) return;
const queue: QueuedMessage[] = JSON.parse(raw);
const remaining: QueuedMessage[] = [];
for (const msg of queue) {
const success = await sendToServer(msg);
if (!success) {
remaining.push({ ...msg, status: 'failed' });
}
}
await AsyncStorage.setItem('pendingMessages', JSON.stringify(remaining));
}
queueMessage writes to local storage immediately and returns — no network call. flushQueue is called when connectivity is detected, iterates through pending messages oldest-first, and removes only the ones that succeed. Failed messages stay in the queue for the next retry.
Notice the ID format: a timestamp plus a random suffix. That's a poor person's globally unique ID, enough for small-scale use. Production systems use proper UUIDs or server-assigned IDs with client-side temporary IDs that get swapped on acknowledgement.
Try implementing this with a real network listener — the NetInfo API on React Native fires an event every time connectivity changes. Wire flushQueue to that event and watch your "pending" messages go live the moment you turn off airplane mode. It is one thing to read about offline queuing, but it is a whole different feeling when you watch your queued messages flush in order the instant the signal comes back.
What's Next
If you are thinking of how media is handled on these gigantic platforms, we have already discussed it in the previous blog. You can check it out here: How Instagram Stores Your Reels, Photos, and Drafts Without Losing a Frame
Happy Exploration!



Top comments (0)