Lesson 6 / 7 · 10 min
Hardening: dedupe, retries, and human takeover
The three failures every live bot meets and the code that survives them. Checkpoint: duplicates ignored, sends retried, admin takeover working.
Three things will happen to your bot in its first production week: the same webhook will arrive twice, a send will fail mid-burst, and a customer will need a human. None of these are edge cases; they are the job. Each gets a small, permanent fix now.
Fix 1: deduplicate by message id
// server.js, first thing inside the POST /webhook handler:
const fresh = db
.prepare("INSERT OR IGNORE INTO processed(message_id) VALUES (?)")
.run(msg.id).changes === 1;
if (!fresh) return; // we already handled this exact messageFix 2: retry sends with backoff
// wa.js
async function withRetry(fn, tries = 3) {
for (let i = 0; i < tries; i++) {
try {
return await fn();
} catch (err) {
if (i === tries - 1) throw err;
await new Promise((r) => setTimeout(r, 1000 * 2 ** i));
}
}
}
// wrap the fetch inside sendText:
// return withRetry(async () => { ...the existing fetch... });Fix 3: a real human takeover
// server.js, inside the webhook handler
// 1. Entering HUMAN state notifies you (set ADMIN_PHONE in .env):
if (session.state === "HUMAN" && previousState !== "HUMAN") {
await sendText(process.env.ADMIN_PHONE,
"Takeover: " + msg.from + " butuh admin. " +
"Balas dia langsung dari WhatsApp Business. " +
"Ketik RESUME " + msg.from + " ke bot untuk menyalakan lagi.");
}
// 2. You can hand the chat back to the bot:
if (msg.from === process.env.ADMIN_PHONE &&
msg.text.body.toUpperCase().startsWith("RESUME ")) {
const phone = msg.text.body.split(" ")[1];
const s = getSession(phone);
s.state = "MENU";
saveSession(s);
await sendText(process.env.ADMIN_PHONE, "Bot aktif lagi untuk " + phone);
return;
}And always: log decisions
Add one console.log per handled message: phone, state before, state after, reply sent. When something odd happens next month, that single line per message is the difference between a five-minute diagnosis and an evening of guessing. In production these lines go to a file or a log service, same idea.
Checkpoint: send a message, restart ngrok mid-conversation, type admin and watch your own phone get the takeover ping, then RESUME the chat. If all three behave, your bot now survives the real world's three favourite punches.
What you take away
- INSERT OR IGNORE on message id makes webhook redelivery harmless
- Exponential backoff (1s, 2s, 4s) absorbs most network failures invisibly
- Takeover pings the admin with context; RESUME hands the chat back
- One log line per message is your future debugging lifeline
Curated sources
Hand-picked references for going deeper, reviewed by our team.