From 2c46007175db9de27783166a6cdfaa704924c404 Mon Sep 17 00:00:00 2001 From: Ching Cheng Kang Date: Mon, 20 Jul 2026 00:26:14 +0800 Subject: [PATCH 1/2] feat: configurable whatsmeow keepalive interval via env whatsmeow pings the websocket on a random interval in [min, max) (default 20-30s). Behind proxies that reap idle tunnels sooner, the ping lands on a dead socket every time, producing constant "Keepalive timed out" reconnect loops (session flapping). Add WAHA_GOWS_KEEPALIVE_INTERVAL_MIN_SEC / _MAX_SEC to override the interval and keep the tunnel warm. Unset (0) leaves the whatsmeow default. max is clamped to > min to avoid a rand panic at ping time. Refs devlikeapro/waha#2167 --- docs/env.md | 18 ++++++++++++++++++ src/env.go | 18 ++++++++++++++++++ src/env_test.go | 19 +++++++++++++++++++ src/main.go | 28 ++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/docs/env.md b/docs/env.md index f641c7b..9c52239 100644 --- a/docs/env.md +++ b/docs/env.md @@ -15,6 +15,24 @@ WAHA_CLIENT_BROWSER_NAME=Firefox WAHA_CLIENT_DEVICE_NAME=Ubuntu ``` +## Keepalive + +whatsmeow pings the websocket on a random interval in `[min, max)` (default 20-30s) +to keep the connection alive. Some proxies (residential/rotating) reap an idle +tunnel sooner than that, so the ping lands on an already-dead socket every time, +producing constant `Keepalive timed out` → reconnect loops (session flapping). + +Lower the interval below the proxy's idle-reap window to keep the tunnel warm. +Both values are in **seconds**; `0` or unset leaves the whatsmeow default. +`max` must be strictly greater than `min` — if it isn't, `max` is adjusted to +`min + 10s` and a warning is logged. + +```bash +# Ping every 8-12s instead of the default 20-30s +WAHA_GOWS_KEEPALIVE_INTERVAL_MIN_SEC=8 +WAHA_GOWS_KEEPALIVE_INTERVAL_MAX_SEC=12 +``` + ## GOWS Device Props Low-level device capability flags sent to WhatsApp during session linking. diff --git a/src/env.go b/src/env.go index e8f4da5..3b63b02 100644 --- a/src/env.go +++ b/src/env.go @@ -18,6 +18,24 @@ func getClientConfig() ClientConfig { return cfg } +// KeepAliveConfig overrides whatsmeow's websocket keepalive ping interval. +// Both values are in seconds; 0 (or unset) leaves whatsmeow's default +// (min 20s / max 30s) in place. Useful behind proxies that reap idle tunnels +// faster than the default ping, which otherwise causes constant +// "Keepalive timed out" reconnect loops. +type KeepAliveConfig struct { + IntervalMinSec uint32 `env:"WAHA_GOWS_KEEPALIVE_INTERVAL_MIN_SEC"` + IntervalMaxSec uint32 `env:"WAHA_GOWS_KEEPALIVE_INTERVAL_MAX_SEC"` +} + +func getKeepAliveConfig() KeepAliveConfig { + cfg := KeepAliveConfig{} + if err := env.Parse(&cfg); err != nil { + panic(err) + } + return cfg +} + // DevicePropsConfig holds optional overrides for waCompanionReg.DeviceProps. // Each Maybe field has three states: // diff --git a/src/env_test.go b/src/env_test.go index 243374a..b949144 100644 --- a/src/env_test.go +++ b/src/env_test.go @@ -117,3 +117,22 @@ func TestFullSyncDaysLimit_Value(t *testing.T) { t.Fatalf("expected Value=30, got %v", cfg.FullSyncDaysLimit.Value) } } + +func TestKeepAliveConfig_NotSet(t *testing.T) { + cfg := getKeepAliveConfig() + if cfg.IntervalMinSec != 0 || cfg.IntervalMaxSec != 0 { + t.Fatalf("expected 0/0 when unset, got %d/%d", cfg.IntervalMinSec, cfg.IntervalMaxSec) + } +} + +func TestKeepAliveConfig_Values(t *testing.T) { + t.Setenv("WAHA_GOWS_KEEPALIVE_INTERVAL_MIN_SEC", "8") + t.Setenv("WAHA_GOWS_KEEPALIVE_INTERVAL_MAX_SEC", "12") + cfg := getKeepAliveConfig() + if cfg.IntervalMinSec != 8 { + t.Fatalf("expected min=8, got %d", cfg.IntervalMinSec) + } + if cfg.IntervalMaxSec != 12 { + t.Fatalf("expected max=12, got %d", cfg.IntervalMaxSec) + } +} diff --git a/src/main.go b/src/main.go index 0bcfeb6..2c3ee8a 100644 --- a/src/main.go +++ b/src/main.go @@ -8,6 +8,7 @@ import ( "github.com/devlikeapro/gows/server" "github.com/devlikeapro/gows/wrpc" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery" + "go.mau.fi/whatsmeow" waLog "go.mau.fi/whatsmeow/util/log" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -99,6 +100,31 @@ func remove(path string) { _ = os.Remove(path) } +// applyKeepAliveConfig overrides whatsmeow's global websocket keepalive ping +// interval from env. whatsmeow pings on a random interval in [min, max) +// (default 20-30s). Behind proxies that reap idle tunnels sooner, the ping +// lands on a dead socket every time, causing constant "Keepalive timed out" +// reconnect loops. Lowering the interval keeps the tunnel warm. Unset values +// (0) leave the whatsmeow default in place. +func applyKeepAliveConfig(log waLog.Logger, cfg KeepAliveConfig) { + if cfg.IntervalMinSec == 0 && cfg.IntervalMaxSec == 0 { + return + } + if cfg.IntervalMinSec > 0 { + whatsmeow.KeepAliveIntervalMin = time.Duration(cfg.IntervalMinSec) * time.Second + } + if cfg.IntervalMaxSec > 0 { + whatsmeow.KeepAliveIntervalMax = time.Duration(cfg.IntervalMaxSec) * time.Second + } + // whatsmeow picks a random interval in [min, max); max must be strictly + // greater than min or rand.Int64N panics at ping time. + if whatsmeow.KeepAliveIntervalMax <= whatsmeow.KeepAliveIntervalMin { + whatsmeow.KeepAliveIntervalMax = whatsmeow.KeepAliveIntervalMin + 10*time.Second + log.Warnf("Keepalive max interval <= min; adjusted max to %s", whatsmeow.KeepAliveIntervalMax) + } + log.Infof("Using keepalive ping interval: min=%s max=%s", whatsmeow.KeepAliveIntervalMin, whatsmeow.KeepAliveIntervalMax) +} + func main() { flag.Parse() log := gowsLog.Stdout("Server", "DEBUG", false) @@ -118,6 +144,8 @@ func main() { log.Infof("Using device name: '%s', browser name: '%s'", clientCfg.DeviceName, clientCfg.BrowserName) gows.SetDeviceAndBrowser(clientCfg.DeviceName, clientCfg.BrowserName) + applyKeepAliveConfig(log, getKeepAliveConfig()) + // Build the server grpcServer := buildGrpcServer(log) // Open unix socket From 0d5bb3325196599268d7f18ba542c9c179ab1a08 Mon Sep 17 00:00:00 2001 From: devlikeapro <155617407+devlikepro@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:54:33 +0700 Subject: [PATCH 2/2] Update src/main.go --- src/main.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main.go b/src/main.go index 2c3ee8a..b3f24f2 100644 --- a/src/main.go +++ b/src/main.go @@ -100,12 +100,6 @@ func remove(path string) { _ = os.Remove(path) } -// applyKeepAliveConfig overrides whatsmeow's global websocket keepalive ping -// interval from env. whatsmeow pings on a random interval in [min, max) -// (default 20-30s). Behind proxies that reap idle tunnels sooner, the ping -// lands on a dead socket every time, causing constant "Keepalive timed out" -// reconnect loops. Lowering the interval keeps the tunnel warm. Unset values -// (0) leave the whatsmeow default in place. func applyKeepAliveConfig(log waLog.Logger, cfg KeepAliveConfig) { if cfg.IntervalMinSec == 0 && cfg.IntervalMaxSec == 0 { return