From 3276bda0c0abef446dd5956d285dfdd55a4c5c4d Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 13 Mar 2026 15:31:33 +0000 Subject: [PATCH] mapper/batcher: replace time.After with NewTimer to avoid timer leak connectionEntry.send() is on the hot path: called once per connection per broadcast tick. time.After allocates a timer that sits in the runtime timer heap until it fires (50 ms), even when the channel send succeeds immediately. At 1000 connected nodes, every tick leaks 1000 timers into the heap, creating continuous GC pressure. Replace with time.NewTimer + defer timer.Stop() so the timer is removed from the heap as soon as the fast-path send completes. --- hscontrol/mapper/batcher.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/hscontrol/mapper/batcher.go b/hscontrol/mapper/batcher.go index becaec71..1d9dc924 100644 --- a/hscontrol/mapper/batcher.go +++ b/hscontrol/mapper/batcher.go @@ -991,12 +991,20 @@ func (entry *connectionEntry) send(data *tailcfg.MapResponse) error { // Use a short timeout to detect stale connections where the client isn't reading the channel. // This is critical for detecting Docker containers that are forcefully terminated // but still have channels that appear open. + // + // We use time.NewTimer + Stop instead of time.After to avoid leaking timers. + // time.After creates a timer that lives in the runtime's timer heap until it fires, + // even when the send succeeds immediately. On the hot path (1000+ nodes per tick), + // this leaks thousands of timers per second. + timer := time.NewTimer(50 * time.Millisecond) //nolint:mnd + defer timer.Stop() + select { case entry.c <- data: // Update last used timestamp on successful send entry.lastUsed.Store(time.Now().Unix()) return nil - case <-time.After(50 * time.Millisecond): + case <-timer.C: // Connection is likely stale - client isn't reading from channel // This catches the case where Docker containers are killed but channels remain open return fmt.Errorf("connection %s: %w", entry.id, ErrConnectionSendTimeout)