Why WhatsApp gateway sessions drop, and what actually fixes it
Most WhatsApp gateway disconnections are not network problems. They are two processes holding one session. Here is the ownership model that prevents it.
When a WhatsApp gateway drops a session, the instinct is to look at the network. Timeouts, socket resets, a flaky upstream. So people add reconnect logic, shorten the retry backoff, add a heartbeat, and the disconnections keep happening.
The network is usually not the problem. The problem is that two processes are holding the same session, and only one of them can be right.
What actually breaks
A linked WhatsApp session is not a stateless API client. It is a Signal protocol endpoint holding a double ratchet — a chain of keys that advances every time a message is encrypted or decrypted. That state is the session. It lives in a database, it is mutated constantly, and it assumes exactly one writer.
Run two processes against one session's state and both advance the ratchet independently. Neither crashes. Nothing logs an error. The keys simply diverge, and from that point the session cannot decrypt what the other side sends. There is no repair procedure. The customer has to unlink the device and scan a QR code again.
For a business running support or order confirmations through that number, a forced re-pair is not an inconvenience. It is an outage that requires someone to physically pick up the phone that owns the account. If that person is on leave, the outage lasts until they are back.
This is why "sessions randomly drop" and "sessions get corrupted" are usually the same bug wearing two hats. A gateway that hasn't solved single ownership will show you both.
How two writers happen
It almost never happens because someone deliberately started two workers. It happens because distributed systems make it easy:
- A deploy overlaps. The new pod starts and connects before the old pod has finished shutting down. For a few seconds, both are live.
- A node goes unreachable. The orchestrator cannot tell "the node is dead" from "the node cannot be reached", so it reschedules the pod elsewhere. The original may still be running and perfectly happy.
- A process pauses. A long garbage-collection pause or a suspended VM freezes a worker past its own timeouts. It wakes up believing it still owns the session, because from the inside nothing happened.
- A retry fires twice. A job that looks failed gets requeued while the first attempt is still connected.
The last two are the interesting ones, because they defeat the fix people usually reach for first.
The fixes that don't work
Checking before you connect. Read a row that says who owns the session, and if it's free, claim it. This is a race with a comfortable-looking gap in the middle: two processes both read "free", both write "mine", both connect. Narrowing the window makes it rarer and therefore harder to diagnose.
Heartbeats alone. A worker writes a timestamp every few seconds; if the timestamp goes stale, another worker takes over. This handles the dead process. It does not handle the paused one. A process that stops for 40 seconds and resumes has no idea time passed. It carries on writing to a session that has been handed to someone else, and now you have two writers — created by the mechanism meant to prevent them.
Leader election alone. Electing one process per shard is sound, but a session is finer-grained than a shard. Rebalancing shards migrates sessions, and the moment of migration is exactly when two owners can exist.
What all three miss is the same thing: it is not enough to decide who owns a session. You have to make it impossible for a previous owner to still write.
What does work
Three mechanisms, layered. None is sufficient alone.
1. An advisory lock, held on a dedicated connection.
Postgres session-scoped advisory locks give mutual exclusion tied to the lifetime of a connection. Take the lock keyed on the session id and hold it for as long as the session is connected. If the process dies, the connection drops and the lock is released by the database — no cleanup job, no stale-state sweeper.
One critical detail: the lock must be taken on a direct connection. Route it through a transaction-mode connection pooler such as PgBouncer or RDS Proxy and the lock is held by whichever pooled backend served that call, which is not the same thing as being held by your process. This turns a correctness primitive into a coin flip.
2. A lease with a monotonically increasing epoch.
Alongside the lock, a row per session recording who holds it, until when, and at which epoch. The owner renews it on a short interval — say every 10 seconds, against a 30 second TTL. Every time ownership changes hands, the epoch increments and never repeats.
The lease is what lets another process take over safely after a genuine failure. The epoch is what makes the takeover final.
3. The epoch as a fencing token on every write.
This is the piece that is usually missing. Every write to session state carries the epoch the writer believes it holds, and the database rejects writes carrying an epoch older than the current one.
Now the paused process is harmless. It wakes up, believes it is the owner, attempts a write with epoch 7 — and the row is at epoch 8 because someone took over while it was frozen. The write is rejected. Not logged and applied. Rejected.
Fencing is what turns "we tried to have one writer" into "a second writer cannot corrupt anything". Without it, the lock and the lease are optimistic. With it, they are enforced.
-- The shape that matters: the epoch is part of the predicate,
-- so a stale writer updates zero rows instead of winning a race.
UPDATE app.session_state
SET blob = $2, updated_at = now()
WHERE session_id = $1
AND epoch = $3;
If that statement reports zero rows affected, the caller is no longer the owner. It should not retry. It should disconnect.
Fail closed, always
The last rule is a policy decision rather than a mechanism, and it is the one that takes discipline to hold.
When lease renewal fails, disconnect immediately.
The temptation is to keep going. The renewal might have failed because of a brief database blip; the session is working; disconnecting a healthy session over a transient error feels like overreacting, and it will show up in your uptime numbers.
Take the disconnection anyway. The two outcomes are not comparable:
| Choice | Best case | Worst case |
|---|---|---|
| Disconnect on renewal failure | Brief reconnect, seconds | Brief reconnect, seconds |
| Continue optimistically | Nothing happens | Ratchet corrupted, customer re-scans a QR code |
A disconnected session reconnects on its own. A corrupted one needs a human holding a phone. When one failure mode is recoverable by software and the other is not, you do not need to know the relative probabilities to know which way to lean.
This is also why a gateway's uptime figure, on its own, tells you very little. A system that never disconnects under partition is not more reliable than one that does — it is one that has chosen the unrecoverable failure to protect the number on the dashboard.
What to ask a gateway
If you are evaluating a WhatsApp gateway, hosted or self-managed, these questions separate the ones that have thought about this from the ones that have not:
- What happens if two workers try to hold the same session? Not "can it happen" — what is the mechanism that stops it.
- Does every write to session state carry a fencing token, or does ownership stop at a lock?
- When the ownership lease cannot be renewed, does the session disconnect or keep running?
- Are advisory locks taken through a connection pooler?
- When a node becomes unreachable, what stops the old process writing?
The answers are more predictive of whether you will be re-scanning QR codes than any uptime percentage, because they describe the failure that uptime does not measure.
The uncomfortable part
None of this makes a session immortal. Sessions still end. WhatsApp can log a device out, a number can be banned, a protocol change can break a library until it is patched. Anyone promising otherwise is selling something.
What single ownership buys is narrower and more valuable than it sounds: the disconnections you get are the recoverable kind. The session comes back on its own, the pairing survives, and nobody has to find the phone.
That is the entire claim. It is not glamorous, and it is most of the difference between a gateway you can run a business on and one you cannot.