Skip to content

handshake: propagate leader load/obtain outcome to all waiters#387

Merged
mholt merged 1 commit into
caddyserver:masterfrom
namepros:np/waiter-outcome-propagation-upstream
Jun 9, 2026
Merged

handshake: propagate leader load/obtain outcome to all waiters#387
mholt merged 1 commit into
caddyserver:masterfrom
namepros:np/waiter-outcome-propagation-upstream

Conversation

@Zenexer

@Zenexer Zenexer commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

getCertDuringHandshake coordinates concurrent handshakes for the same name through certLoadWaitChans. If multiple concurrent handshakes occur, they race; the winner loads or obtains a cert, while others wait.

However, the winner's result is only correctly propagated to waiters on the external-cert-manager path (#369). On every other path (on-demand permission denial, a failed obtain, or a successful load whose cert is evicted from the cache before waiters re-check it), losers receive nothing and recursively re-enter the wait queue behind the next winner.

Under sustained handshake load for names that cannot get certificates (e.g. on-demand TLS receiving scanner traffic, or an ask endpoint rejecting/throttling), these recursing goroutines accumulate.

This leak was observed in production on caddy 2.11.x, on-demand TLS, ~36 req/s:

  • 30k+ goroutines per process at recursion depths up to 48
  • Roughly 1.6GB/min of working-set growth

Ideally, ask endpoints should be fast, so this should be a non-issue; in practice, that isn't always the case.

This was debugged and patched largely by Claude, Opus 4.8. I monitored what it was doing and reworked some of its wording, but it seems to have correctly identified the issue without much assistance.

Comment thread handshake.go Outdated
@Zenexer
Zenexer force-pushed the np/waiter-outcome-propagation-upstream branch from bd1e865 to 7898be0 Compare June 8, 2026 14:06
When the goroutine leading a certificate load/obtain for a name fails
(e.g. an on-demand permission denial) or succeeds but the cert is
evicted from the cache before waiters re-look it up, waiters
recursively re-enter the wait queue instead of receiving the leader's
outcome. Under sustained handshake load for uncertificated names,
these recursing goroutines accumulate without bound (observed at 30k+
goroutines, recursion depth 48), each pinning live TLS handshake
state — multi-GB memory growth within minutes.

Propagate the leader's returned certificate or error to all waiters
via the existing certLoadWaiter fields (previously populated only on
the external-cert-manager path). Waiters now return the leader's
outcome directly; the recursive fallback remains only for the rare
empty-cert+nil-error case.
@Zenexer
Zenexer force-pushed the np/waiter-outcome-propagation-upstream branch from 7898be0 to 0c049a3 Compare June 8, 2026 14:10

@mholt mholt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this seems correct, I believe.

@mholt
mholt merged commit cf6f57d into caddyserver:master Jun 9, 2026
6 checks passed
@Zenexer
Zenexer deleted the np/waiter-outcome-propagation-upstream branch June 19, 2026 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants