Build1 publisher3 min readPublished
A WhatsApp line sat dead for 86 hours behind a container Docker called healthy
Seven small businesses share one self-hosted WAHA container. Three of their lines failed in nine days while the process stayed responsive, and the watcher written to catch the next one did not run from cron for five days.
The Engineer · Build desk

What happened
- A check at 08:33 on 14 September found an insurance agency's WhatsApp session in FAILED state with its last activity stamped 10 September at 18:09, eighty-six hours earlier, and every dashboard green.
- Three of the seven business lines sharing one self-hosted WAHA container went down between 4 and 13 September.
- The ten-minute cron watcher built on 8 September to catch exactly this class of failure did not run once until 13 September.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure The unlink that killed the clinic's line happened on the client's phone, so a customer tidying up old devices can take down billing-relevant automation without touching the operator's infrastructure.
- constraint Alert routing cannot be built on the API being monitored: identity survives a connection failure and not a removed device, so an external roster is mandatory and the accounting system cannot supply one.
- decision Anyone bridging many tenants through one process now has to define readiness per session, which means the probe enumerates tenants and somebody owns that list.
- precedent Monitoring that runs on a schedule needs its own proof of execution, because a watcher that passed its tests and never fired leaves the same silence as no watcher at all.
The monitor checked one thing: that the waha container was running and responsive [4]. It was, through all three outages [4]. The post calls this the ordinary liveness-versus-readiness mistake, with the difference that the readiness that mattered belonged to seven independent sessions inside the service, each able to fail on its own, for unrelated reasons, without the process noticing [2].
The first outage sets the polling rate. That line failed on 4 September at 20:47 and was found on the 8th, and across those three and a half days the container reported itself healthy roughly five hundred times [5]. Three and a half days is 5,040 minutes, so the healthy answer came back about every ten minutes [1].
The second failure was invisible on both sides. On 5 September at 11:04 the gateway logged a "device removed stream error" for a clinic's line, after the linked device was removed from the client's phone [6]. Messages kept arriving on the owner's phone and customers kept getting replies, because a human was reading the phone [7]. The CRM went blind and the automations stopped: the pre-appointment reminders, the routing, the logging, the follow-ups [8]. "If your failure mode is invisible to the user and invisible to your health check, you do not have a monitoring gap. You have no monitoring," the post's author wrote [9].
Writing the alerting turned up a second dependency. While a session is WORKING, /api/sessions returns me.id and me.pushName; a teardown by unlink wipes both fields [10]. The insurance line, failed for eighty-six hours, still carried its me block when the API was read on 14 September [12]. The author had recorded the wipe as an absolute rule in notes the week before and corrected it in the post: a removed device wipes identity, a connection-level failure does not, so the field cannot be depended on [13]. The fix keeps a local roster.json, rewritten on every pass but only from sessions currently in WORKING, so that a single pass during an outage cannot overwrite good identity with nulls [14].
Billing does not recover the mapping. A scan of all 141 customers in the accounting system returned zero matches for these business numbers, because an invoice carries the owner's personal mobile and the bot runs on a different line [15].
The watcher itself went in on 8 September: a ten-minute cron job that diffs session states, notifies the business owner when their line drops, and sends them a pairing code to reconnect it themselves [16]. It passed ten test scenarios and caught three real bugs before install [17]. It did not run once from cron until 13 September [18]. The insurance agency's line went quiet on 10 September at 18:09, inside that gap [2]. The crontab entry calls sudo and appends to /opt/waha-watch/cron.log [19], and /opt/waha-watch is owned by root [20]; the post does not say which of the two stopped it.
For any of this to transfer, one thing has to be true of your stack: the wrapper can stay up while a single tenant's downstream session dies for reasons of its own. Where that holds, the readiness probe has to enumerate tenants and assert per-tenant state, and it needs a second check that proves the prober ran at all.
What to watch
- Whether WAHA exposes a per-session state endpoint a container health check can consume, which would move per-tenant readiness into the platform instead of a side script.
- Whether non-technical business owners actually use the pairing code to reconnect their own line, or recovery stays a manual visit.
- Whether the watcher and the WORKING-only roster.json write rule get published, so others can check the identity-overwrite guard.