Skip to content

fix: memory leak in UDP transport acceptedAddr slice#286

Merged
emiago merged 1 commit intoemiago:mainfrom
udzin:udp_leak_fix
Feb 5, 2026
Merged

fix: memory leak in UDP transport acceptedAddr slice#286
emiago merged 1 commit intoemiago:mainfrom
udzin:udp_leak_fix

Conversation

@udzin
Copy link

@udzin udzin commented Feb 5, 2026

Replace unbounded slice growth with map to store unique remote addresses. The acceptedAddr slice was growing indefinitely in server scenarios with multiple clients, causing memory leak founded by pprof analysis.

(pprof) top
Showing nodes accounting for 79.61MB, 100% of 79.61MB total
Showing top 10 nodes out of 62
      flat  flat%   sum%        cum   cum%
      42MB 52.76% 52.76%       42MB 52.76%  net.JoinHostPort (inline)
   33.09MB 41.57% 94.33%    75.09MB 94.33%  github.com/emiago/sipgo/sip.(*TransportUDP).readListenerConnection
    1.50MB  1.89% 96.22%     1.50MB  1.89%  runtime.malg
       1MB  1.26% 97.48%        1MB  1.26%  google.golang.org/protobuf/internal/filedesc.(*Message).unmarshalFull
    0.51MB  0.64% 98.11%     0.51MB  0.64%  github.com/rcrowley/go-metrics.newExpDecaySampleHeap
    0.50MB  0.63% 98.74%     0.50MB  0.63%  github.com/IBM/sarama.NewBroker
    0.50MB  0.63% 99.37%     0.50MB  0.63%  context.(*cancelCtx).propagateCancel
    0.50MB  0.63%   100%     0.50MB  0.63%  runtime.(*scavengerState).init
         0     0%   100%     0.50MB  0.63%  context.WithCancel
         0     0%   100%     0.50MB  0.63%  context.withCancel (inline)

(pprof) list .readListenerConnection
Total: 79.61MB
ROUTINE ======================== github.com/emiago/sipgo/sip.(*TransportUDP).readListenerConnection in github.com/emiago/sipgo@v1.0.0/sip/transport_udp.go
   33.09MB    75.09MB (flat, cum) 94.33% of Total
         .          .    123:func (t *TransportUDP) readListenerConnection(conn *UDPConnection, laddr string, handler MessageHandler) {
         .          .    124:	buf := make([]byte, TransportBufferReadSize)
         .          .    125:	defer func() {
         .          .    126:		if err := t.pool.CloseAndDelete(conn, laddr); err != nil {
         .          .    127:			t.log.Warn("connection pool not clean cleanup", "error", err)
         .          .    128:		}
         .          .    129:	}()
         .          .    130:	defer t.log.Debug("Read listener connection stopped", "laddr", laddr)
         .          .    131:
         .          .    132:	var lastRaddr string
         .          .    133:	// NOTE: consider to refactor, but for cleanup
         .          .    134:	// We are reusing UDP listener as dial connection
         .          .    135:	acceptedAddr := make([]string, 0, 1000)
         .          .    136:	defer func() {
         .          .    137:		t.pool.DeleteMultiple(acceptedAddr)
         .          .    138:	}()
         .          .    139:
         .          .    140:	for {
         .          .    141:		num, raddr, err := conn.ReadFrom(buf)
         .          .    142:		if err != nil {
         .          .    143:			if errors.Is(err, net.ErrClosed) {
         .          .    144:				t.log.Debug("Read connection closed", "laddr", laddr, "error", err)
         .          .    145:				return
         .          .    146:			}
         .          .    147:			t.log.Error("Read connection error", "laddr", laddr, "error", err)
         .          .    148:			return
         .          .    149:		}
         .          .    150:
         .          .    151:		data := buf[:num]
         .          .    152:		if len(bytes.Trim(data, "\x00")) == 0 {
         .          .    153:			continue
         .          .    154:		}
         .       42MB    155:		rastr := raddr.String()
         .          .    156:		if lastRaddr != rastr {
         .          .    157:			// In most cases we are in single connection mode so no need to keep adding in pool
         .          .    158:			// In case of server and multiple UDP listeners, this makes sure right one is used
         .          .    159:			t.pool.Add(rastr, conn)
   33.09MB    33.09MB    160:			acceptedAddr = append(acceptedAddr, rastr)
         .          .    161:		}
         .          .    162:
         .          .    163:		t.parseAndHandle(data, rastr, handler)
         .          .    164:		lastRaddr = rastr
         .          .    165:	}

Replace unbounded slice growth with map to store unique remote addresses.
The acceptedAddr slice was growing indefinitely in server scenarios with
multiple clients, causing ~75MB memory leak as shown by pprof analysis.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@emiago emiago merged commit 719596c into emiago:main Feb 5, 2026
1 check passed
@emiago
Copy link
Owner

emiago commented Feb 5, 2026

Yeah it makes sense. It was left to refactor. Personally I use TCP/TLS on endpoints, so probably never had to revisit this.
Thanks. Merging

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