Summary
OpenClaw's WhatsApp channel health monitor restarts the WhatsApp Web socket every ~35 minutes on personal/low-traffic accounts. This creates a perpetual restart cycle that causes cron job delivery failures when jobs fire during the brief reconnection window. We observed 4,004 stale-socket restarts in 17 days and applied a config mitigation (channelHealthCheckMinutes: 360). We believe the root cause warrants a product fix: making staleEventThresholdMs configurable.
OpenClaw version: 2026.3.13
Platform: macOS (Darwin 25.2.0), local installation
WhatsApp: Multi-device, personal account
Active cron jobs: ~68 across 14 agents
The Problem
What we observed
Starting 2026-03-02, our gateway log showed a continuous pattern:
2026-03-18T15:39:40 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
2026-03-18T16:14:40 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
2026-03-18T16:49:40 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
2026-03-18T17:24:40 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
...
This pattern repeats 24/7, including overnight, weekends, and any quiet period.
Total stale-socket restarts: grep -c 'stale-socket' gateway.log → 4,004 over 17 days (~235/day).
Impact on cron jobs
We run ~68 active cron jobs across 14 agents (family reminders, daily digests, memory maintenance, weekly reviews). When a cron job fires during the brief window where WhatsApp is reconnecting, it fails with:
Error: No active WhatsApp Web listener (account: default).
Start the gateway, then link WhatsApp with: openclaw channels login --channel whatsapp --account default.
This caused 6 cron jobs to accumulate consecutive errors, including family reminders, morning digest delivery, and nightly memory retention jobs.
The error message is misleading — WhatsApp IS linked and the gateway IS running. The socket is simply in the middle of a health-monitor-triggered restart cycle.
Root Cause Analysis
How the health monitor works
We traced the behavior through the source code:
Layer 1: External health monitor (channel-health-policy.ts)
- Runs every
channelHealthCheckMinutes (default: 5 minutes)
- For each connected channel, checks
lastEventAt timestamp
- If no event has been received for
DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS (30 minutes, hardcoded at line 54), classifies the socket as "stale-socket"
- Action: stops and restarts the channel
Layer 2: Internal watchdog (monitor.ts)
- Independent from Layer 1
MESSAGE_TIMEOUT_MS = 30 * 60 * 1000 (30 minutes)
- Checks every 60 seconds
- If no message received for 30+ minutes, calls
listener.signalClose() with status 499
Both layers use the same 30-minute threshold. Neither is configurable via openclaw.json.
Why it creates a restart loop on personal accounts
The logic assumes: "If no WhatsApp event arrives in 30 minutes, the socket is probably dead."
This assumption is valid for high-traffic business accounts but false for personal accounts where:
- Overnight quiet periods (23:00–07:00) routinely have zero messages for 8+ hours
- Weekend mornings may have 2-3 hour gaps between messages
- Even during the day, a family account can easily go 30-60 minutes without a message
The result: Every 30 minutes of legitimate silence, the health monitor tears down a perfectly healthy WebSocket connection and restarts it. The restart takes a few seconds, during which the WhatsApp listener is unavailable. If a cron job fires in that window, it fails.
The restart cycle timeline
T+0:00 Health monitor checks → socket connected, lastEvent was 31 min ago → "stale-socket" → RESTART
T+0:05 Socket reconnects → WhatsApp Web connected → healthy
T+0:35 Health monitor checks → socket connected, lastEvent was 30 min ago → "stale-socket" → RESTART
T+0:40 Socket reconnects → healthy
T+1:10 Health monitor checks → stale again → RESTART
...repeat forever
With the default 5-minute check interval and 30-minute stale threshold, this produces a restart every ~35 minutes = ~41 restarts/day.
Source Code References
| Component |
File |
Line |
Value |
| Stale threshold (hardcoded) |
src/gateway/channel-health-policy.ts |
54 |
DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS = 30 * 60_000 |
| Health check interval (configurable) |
src/gateway/server.impl.ts |
758-765 |
gateway.channelHealthCheckMinutes (default 5) |
| Disable check (special case) |
src/gateway/server.impl.ts |
759 |
channelHealthCheckMinutes === 0 → monitor disabled |
| Config schema |
src/config/zod-schema.ts |
698 |
z.number().int().min(0).optional() |
| Internal watchdog timeout |
src/web/auto-reply/monitor.ts |
— |
MESSAGE_TIMEOUT_MS = 30 * 60 * 1000 |
| Internal watchdog check |
src/web/auto-reply/monitor.ts |
— |
WATCHDOG_CHECK_MS = 60 * 1000 |
Current Mitigation
We set channelHealthCheckMinutes: 360 to reduce restarts from ~41/day to ~4/day:
"gateway": {
"channelHealthCheckMinutes": 360
}
| Option |
Effect |
Trade-off |
0 (disable) |
No health monitoring |
Too risky — genuine dead sockets go undetected |
60 (1 hour) |
~24 restarts/day |
Reduces but doesn't solve |
360 (6 hours) |
~4 restarts/day |
Chosen — safety net preserved, quiet periods survivable |
This is a workaround, not a fix. The check interval controls how often the monitor runs, not what it considers stale. Even at 360 minutes, a quiet channel is still classified as stale at each check.
Feature Request
Make staleEventThresholdMs configurable
Add a config key to control the stale-socket detection threshold:
"gateway": {
"channelHealthCheckMinutes": 5,
"channelStaleEventThresholdMinutes": 360
}
This would allow the health monitor to run frequently (catching genuine outages quickly) while tolerating long quiet periods on personal accounts.
Why the check interval alone is insufficient: With channelHealthCheckMinutes: 5 and a configurable stale threshold of 360 minutes, the monitor would check every 5 minutes but only restart if the socket has been truly silent for 6 hours. This is the ideal configuration — fast detection of real problems, zero false positives from normal quiet periods.
Suggested defaults
- Current 30-minute default is reasonable for high-traffic business accounts
- For personal accounts, 120-360 minutes would be appropriate
- A documented recommendation in the configuration docs would help users choose
Additional consideration: internal watchdog
The internal watchdog in monitor.ts also uses a 30-minute timeout (MESSAGE_TIMEOUT_MS). If the external health monitor threshold becomes configurable, the internal watchdog should either:
- Respect the same config value, or
- Be independently configurable, or
- Be documented as a separate recovery mechanism with its own fixed threshold
Evidence
| Evidence |
Source |
Finding |
| 4,004 stale-socket restarts |
grep -c 'stale-socket' gateway.log |
Continuous since 2026-03-02 |
| ~35-minute cycle |
grep 'stale-socket' gateway.log timestamps |
~35min intervals |
| 6 cron jobs in error state |
openclaw cron list |
"No active WhatsApp Web listener" |
| WhatsApp linked during errors |
openclaw doctor --non-interactive |
"WhatsApp: linked (auth age Xm)" confirmed while cron errors occurred |
| Config key valid |
src/config/zod-schema.ts:698 |
z.number().int().min(0).optional() — 360 passes |
| Threshold hardcoded |
src/gateway/channel-health-policy.ts:54 |
No config override available |
Summary
OpenClaw's WhatsApp channel health monitor restarts the WhatsApp Web socket every ~35 minutes on personal/low-traffic accounts. This creates a perpetual restart cycle that causes cron job delivery failures when jobs fire during the brief reconnection window. We observed 4,004 stale-socket restarts in 17 days and applied a config mitigation (
channelHealthCheckMinutes: 360). We believe the root cause warrants a product fix: makingstaleEventThresholdMsconfigurable.OpenClaw version: 2026.3.13
Platform: macOS (Darwin 25.2.0), local installation
WhatsApp: Multi-device, personal account
Active cron jobs: ~68 across 14 agents
The Problem
What we observed
Starting 2026-03-02, our gateway log showed a continuous pattern:
This pattern repeats 24/7, including overnight, weekends, and any quiet period.
Total stale-socket restarts:
grep -c 'stale-socket' gateway.log→ 4,004 over 17 days (~235/day).Impact on cron jobs
We run ~68 active cron jobs across 14 agents (family reminders, daily digests, memory maintenance, weekly reviews). When a cron job fires during the brief window where WhatsApp is reconnecting, it fails with:
This caused 6 cron jobs to accumulate consecutive errors, including family reminders, morning digest delivery, and nightly memory retention jobs.
The error message is misleading — WhatsApp IS linked and the gateway IS running. The socket is simply in the middle of a health-monitor-triggered restart cycle.
Root Cause Analysis
How the health monitor works
We traced the behavior through the source code:
Layer 1: External health monitor (
channel-health-policy.ts)channelHealthCheckMinutes(default: 5 minutes)lastEventAttimestampDEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS(30 minutes, hardcoded at line 54), classifies the socket as"stale-socket"Layer 2: Internal watchdog (
monitor.ts)MESSAGE_TIMEOUT_MS = 30 * 60 * 1000(30 minutes)listener.signalClose()with status 499Both layers use the same 30-minute threshold. Neither is configurable via
openclaw.json.Why it creates a restart loop on personal accounts
The logic assumes: "If no WhatsApp event arrives in 30 minutes, the socket is probably dead."
This assumption is valid for high-traffic business accounts but false for personal accounts where:
The result: Every 30 minutes of legitimate silence, the health monitor tears down a perfectly healthy WebSocket connection and restarts it. The restart takes a few seconds, during which the WhatsApp listener is unavailable. If a cron job fires in that window, it fails.
The restart cycle timeline
With the default 5-minute check interval and 30-minute stale threshold, this produces a restart every ~35 minutes = ~41 restarts/day.
Source Code References
src/gateway/channel-health-policy.tsDEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS = 30 * 60_000src/gateway/server.impl.tsgateway.channelHealthCheckMinutes(default 5)src/gateway/server.impl.tschannelHealthCheckMinutes === 0→ monitor disabledsrc/config/zod-schema.tsz.number().int().min(0).optional()src/web/auto-reply/monitor.tsMESSAGE_TIMEOUT_MS = 30 * 60 * 1000src/web/auto-reply/monitor.tsWATCHDOG_CHECK_MS = 60 * 1000Current Mitigation
We set
channelHealthCheckMinutes: 360to reduce restarts from ~41/day to ~4/day:0(disable)60(1 hour)360(6 hours)This is a workaround, not a fix. The check interval controls how often the monitor runs, not what it considers stale. Even at 360 minutes, a quiet channel is still classified as stale at each check.
Feature Request
Make
staleEventThresholdMsconfigurableAdd a config key to control the stale-socket detection threshold:
This would allow the health monitor to run frequently (catching genuine outages quickly) while tolerating long quiet periods on personal accounts.
Why the check interval alone is insufficient: With
channelHealthCheckMinutes: 5and a configurable stale threshold of 360 minutes, the monitor would check every 5 minutes but only restart if the socket has been truly silent for 6 hours. This is the ideal configuration — fast detection of real problems, zero false positives from normal quiet periods.Suggested defaults
Additional consideration: internal watchdog
The internal watchdog in
monitor.tsalso uses a 30-minute timeout (MESSAGE_TIMEOUT_MS). If the external health monitor threshold becomes configurable, the internal watchdog should either:Evidence
grep -c 'stale-socket' gateway.loggrep 'stale-socket' gateway.logtimestampsopenclaw cron listopenclaw doctor --non-interactivesrc/config/zod-schema.ts:698z.number().int().min(0).optional()— 360 passessrc/gateway/channel-health-policy.ts:54