Skip to content

[bug]: [wtclient] catch-up after a prolonged tower outage starves a running node — forwarding stops, gRPC hangs, one Postgres txn left open #10994

Description

@Wired4ncer

Pre-Submission Checklist

  • I have searched the existing issues and believe this is a new bug.
  • I am not asking a question about how to use lnd, but reporting a bug (otherwise open a discussion).

LND Version

v0.21.1-beta (both the client node and the watchtower server node)

LND Configuration

Client:

wtclient.active=true
wtclient.sweep-fee-rate=100
db.backend=postgres
db.postgres.maxconnections=25
db.postgres.timeout=0
db.postgres.channeldb-with-global-lock=false

wtclient.db was migrated into Postgres in Dec 2025 (use-native-sql=true), so the
"delete wtclient.db and restart" workaround from older threads is not available.

Tower (separate host):

watchtower.active=true
watchtower.listen=0.0.0.0:9911

Backend Version

bitcoind (client node is fully synced throughout; the backend is not implicated)

OS/Distribution

Ubuntu, x86_64, 8 vCPU / 16 GB, lnd under systemd

Bug Details & Steps to Reproduce

Two phases. Phase 1 is the known failure; phase 2 is what I think is new.

Phase 1 — tower rejects everything for a long period.
The tower rejected every state update from this client with CodePermanentFailure and
StateUpdateCodeSeqNumOutOfOrder. Client-side seqnums were frozen; WTCL=debug showed
Unable to get new tower candidate ... reason: exhausted all tower candidates on every client
type. Duration is unknown — it predates log retention on both hosts. lncli wtclient towers,
stats and remove all hung during this period. This part matches #6397 / #4420 / #6785.

Restarting lnd on the tower (not the client) cleared it. Within ~20 minutes each client
type negotiated a fresh session and uploads resumed with code=0 acks and in-order seqnums.

Phase 2 — the catch-up then took the node down.
The client began a sustained catch-up and held it for ~3 hours:

  • ~100 state updates/min accepted, flat, no taper
  • one new session acquired per ~10 min (consistent with the 1024-updates-per-session cap in [watchtower] [feature]: mark exhausted sessions as inactive #7045)
  • sessions(acquired=…) went 202 → 219, exhausted 205 → 222 over that window
  • meanwhile the node's own live backup demand was ~7 tasks/hour
    (tasks(received=255) total across 36 h of uptime, against accepted=226526)

After ~3 hours the node had effectively stopped working as a routing node, while
systemctl still reported lnd active:

Signal Value
HTLCs forwarded, last 10 min 0
Peers 61, down from 110
pong response failure warnings 38 in 14 min
lncli getinfo 89.7 s
gRPC clients (a Django/gunicorn dashboard) killed on their 120 s timeout, respawning
Postgres one lnd connection idle in transaction for 30m50s
all other lnd Postgres connections idleno query active anywhere
CPU / iowait ~74% idle, 0–1% iowait

The last three rows are the interesting part: lnd was not blocked waiting on the database.
Nothing was executing and no backend was waiting on a lock — lnd had stopped issuing work,
while leaving one transaction open. With db.postgres.timeout=0 nothing ever times out, so the
condition does not self-clear.

Resolution: set wtclient.active=false, restart lnd. Immediately after:

Signal Before After
lncli getinfo 89.7 s 1.9 s
Forward/settle activity, 10 min 0 33
Peers 61 77 and climbing
Longest idle in transaction 30m50s none
Load average 4.54 1.00

Expected Behavior

A watchtower backlog is not urgent work — the node is online and self-watching. It should be
paced or backgrounded such that it cannot starve the htlcswitch, the peer subsystem and the RPC
server. At minimum, an unbounded catch-up should not be able to leave a Postgres transaction
open indefinitely while the client makes no progress elsewhere.

Debug Information

Sanitised log shapes (client):

[INF] WTCL: (anchor) Client stats: tasks(received=255 accepted=226526 ineligible=0) sessions(acquired=219 exhausted=222)

Tower side, with WTWR=debug (120 s sample, IDs redacted):

[DBG] WTWR: Received MsgStateUpdate(seqnum=177 last_applied=176 is_complete=0 hint=<redacted>) from <session-key>
[DBG] WTWR: State update 177 accepted for <session-key>
[DBG] WTWR: Sending MsgStateUpdateReply(code=0 last_applied=177) to <session-key>

112 unique hints in that window, and a second window ~13 min later shared zero hints with
the first — so the client was sending genuinely new states, not replaying.

Also, and this may be the more actionable half: lncli wtclient towers --include_sessions
never returned — killed after 18m16s having produced no output. An earlier call was killed
at 25 s. getinfo was measured at 89.7 s while that call was outstanding, and killing the
client process did not restore responsiveness. This looks like #6660 (that reporter saw
10.9 GB RES and a 6m57s runtime, with the same trigger: data queued up while the tower was
down). It makes the one RPC you need for diagnosis unusable exactly when you need it.

Related

Caveat on my own data: my exhausted-session count (222) is an order of magnitude below #7121's
2,116, so I am not certain the candidate loop alone explains the stall — the hung RPC may be
carrying more of it. Happy to gather more if there is something specific worth capturing before
I re-enable the client.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions