If you restored your iPhone from a backup and now WhatsApp receives messages but every outgoing message fails with a red ❗, this repo explains why and how to fix it — and gives you scripts to do it.
The cause is almost always a single corrupt database inside WhatsApp's
container: MessagingInfraDB_v2/MessagingInfraDatabase.sqlite. It's the
outbound send-queue plumbing. When it's malformed, incoming messages still
land (they go into ChatStorage.sqlite, which is fine), but nothing can be
sent. Reinstalling WhatsApp, re-verifying your number, removing passkeys/email,
toggling backup encryption — none of it helps, because none of it touches that
file. And every restore just re-installs the same corrupt copy.
⚠️ Disclaimer: this edits iOS backups on your own machine. It worked for the author, but it is provided as-is with no warranty. Keep a copy of your backup, and don't run it on data you can't afford to lose. Editing backups is for your own device and data only.
- WhatsApp receives fine, but every outgoing message shows a red ❗, in every chat.
- It started only after restoring a device backup (Finder/iTunes or iCloud).
- It survives reinstalling the app and re-verifying your number.
- A fresh install with no restore can send — but has no history.
- A linked companion (WhatsApp Web / Desktop / a linked phone) can send — proof your account and number are healthy; only this device's send-path is broken.
If that matches, run the diagnostic below.
- macOS (or any machine with your iOS backup), Python 3, and the
sqlite3CLI.- The repair step (
wa_fix_send.py) shells out to thesqlite3command-line tool. Python bundles the sqlite3 module but not the CLI, so on Windows you usually need to install it:winget install SQLite.SQLite, or download the "sqlite-tools" bundle from https://sqlite.org/download.html and add the folder containingsqlite3.exeto yourPATH. macOS ships it; on Linux usesudo apt install sqlite3. (wa_send_diagnose.pydoesn't need the CLI.)
- The repair step (
- For encrypted backups (recommended):
pip install iphone_backup_decrypt pycryptodome - An iOS device backup made by Finder/iTunes (encrypted strongly preferred — see below).
WhatsApp has its own optional end-to-end encrypted backup (WhatsApp →
Settings → Chats → Chat Backup → End-to-end encrypted backup). While it is
on, WhatsApp excludes your chat history from iOS device backups
entirely: ChatStorage.sqlite, contacts, chat search, and media are simply
not in the Finder/iTunes (or iCloud device) backup. They are not "in there but
locked" — they are absent. Only WhatsApp's own iCloud chat backup (protected by
the 64-digit key / password) still has them.
For this repair that means:
- The diagnosis still works —
MessagingInfraDatabase.sqliteis not covered by the E2E exclusion and stays in the device backup. - But restoring such a device backup gives you a WhatsApp without local chat history. Your history would have to come from WhatsApp's own E2E iCloud chat backup — which, per Step 4 below, may carry the corruption right back onto the phone.
So before making the device backup you intend to fix and restore:
- WhatsApp → Settings → Chats → Chat Backup → turn OFF end-to-end encrypted backup.
- Make a fresh encrypted Finder backup.
- Run the diagnose script and confirm
ChatStorage.sqliteappears in the table (that proves your history is really in the backup). - After the repair and restore are done, turn E2E encrypted backup back on if you want it — and refresh the iCloud chat backup from the healthy phone (Step 4).
python3 scripts/wa_send_diagnose.py /path/to/MobileSync/Backup/<backup-id>
# or, with no argument, it lists the backups it can findIt decrypts (if needed) and runs PRAGMA quick_check on every WhatsApp
database. The smoking gun:
MessagingInfraDB_v2/MessagingInfraDatabase.sqlite *** MALFORMED ***
ChatStorage.sqlite ok
(Two databases — emoji.sqlite and the chat-search index — may show an
"unknown tokenizer" note. That's not corruption; WhatsApp uses custom
full-text-search tokenizers that plain sqlite3 can't load. The script labels
them accordingly.)
You may also see WARN: decrypted X bytes ... expected Y bytes lines. Those
come from comparing the decrypted size against the size recorded in
Manifest.db, which is often stale even in perfectly healthy backups. Only the
quick_check verdict in the status column counts.
If ChatStorage.sqlite (and ContactsV2.sqlite, media, chat search) is
missing from the table entirely, your backup was made with WhatsApp's
end-to-end encrypted backup turned on — see the warning below.
python3 scripts/wa_fix_send.py /path/to/Backup/<backup-id> # dry run
python3 scripts/wa_fix_send.py /path/to/Backup/<backup-id> --apply # do itIt salvages a clean MessagingInfraDatabase with sqlite3 .recover (the file
holds only empty send queues + receipt rows + schema bookkeeping — no
messages, no keys, so a rebuilt copy is safe), then swaps it into the backup:
- Unencrypted backup: replaces the file and fixes the stored size in
Manifest.db. - Encrypted backup: re-encrypts the file with its own per-file key and
re-encrypts
Manifest.db(iOS uses AES-256-CBC, zero IV, PKCS7 padding; the per-file key is reused, not re-wrapped).
Originals it touches are snapshotted to wa_fix_snapshot_<id>/ next to the
backup, so you can revert.
Finder → select the iPhone → Restore Backup… → pick the edited backup. (Find My must be off. Be patient — the restore can look stuck for a while before completing.)
Encrypted backup (recommended): the Keychain is restored, so WhatsApp stays logged in and just opens the local, now-healthy data. Sending works, history intact, no re-verification.
Unencrypted backup: the Keychain is not in the backup, so WhatsApp re-registers. After the restore, before opening WhatsApp:
- Turn off iCloud for WhatsApp (Settings → iCloud → WhatsApp → Off), so it can't pull a (possibly still-poisoned) iCloud backup.
- Open WhatsApp, verify by SMS.
- It adopts the local restored data → sending works, history intact.
If your broken phone ever uploaded a WhatsApp iCloud chat backup, that backup carries the corruption too — restoring from it later will re-break sending. Once the phone is healthy:
- Delete the old WhatsApp iCloud chat backup (WhatsApp → Settings → Chats → Chat Backup, and/or iCloud storage management).
- Make a fresh chat backup from the now-healthy phone.
Now both the device and the cloud are clean.
The corruption is a one-time event during a backup/restore cycle — the
send-queue database gets written malformed. From then on it's carried in every
backup (Finder and iCloud) and faithfully re-installed on each restore. The
restore process itself does not regenerate the corruption: a healthy copy
restored stays healthy. WhatsApp's launch path simply doesn't validate or
rebuild a malformed MessagingInfraDatabase — it fails to send instead of
self-healing.
On launch/import, integrity-check MessagingInfraDatabase and rebuild it if
malformed (it's regenerable plumbing). That one defensive check would
eliminate this entire "receive-but-not-send after restore" class of failure.
| File | Purpose |
|---|---|
scripts/wa_send_diagnose.py |
Integrity-check every WhatsApp DB in a backup; flag the malformed one. |
scripts/wa_fix_send.py |
Recover the DB and patch it back into the backup (encrypted or unencrypted). |
tests/ |
Regression tests + a synthetic, PII-free fixture generator. |
Passwords are read interactively (getpass) and never stored or logged.
python3 -m unittest discover testsThe tests never use a real backup. Fixtures are generated at run time from the
schema only (tests/infra_schema.py — table/column names, no rows), so the
MessagingInfraDatabase fixture carries no JIDs, phone numbers, or message
content. tests/pii_scan.py scans the raw fixture bytes (and the .recover
output) on every run and fails if it finds any JID/phone pattern — so "is there
PII in here?" is answered by the test, not by trust.
Never commit a real backup or database. The infra DB looks harmless (the send
queues are empty), but its receipt_device table holds the JIDs of everyone you
exchange receipts with — your contact graph — and .recover pulls those back out
even of a corrupt copy. .gitignore blocks *.sqlite; keep real backups
outside the repo.