grpc/grpc-go · error

error getting option on socket: %v

Error message

error getting option on socket: %v

What it means

GetTCPUserTimeout reads the TCP_USER_TIMEOUT socket option via GetsockoptInt inside a rawConn.Control() callback. This error fires when the Control() call itself fails (the underlying syscall returned an errno), meaning gRPC could not query the kernel-level keepalive timeout that the OS has set on this connection's file descriptor. It is produced by gRPC's internal syscall package, not by application code.

Source

Thrown at internal/syscall/syscall_linux.go:107

}

// GetTCPUserTimeout gets the TCP user timeout on a connection's socket
func GetTCPUserTimeout(conn net.Conn) (opt int, err error) {
	tcpconn, ok := conn.(*net.TCPConn)
	if !ok {
		err = fmt.Errorf("conn is not *net.TCPConn. got %T", conn)
		return
	}
	rawConn, err := tcpconn.SyscallConn()
	if err != nil {
		err = fmt.Errorf("error getting raw connection: %v", err)
		return
	}
	err = rawConn.Control(func(fd uintptr) {
		opt, err = syscall.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_USER_TIMEOUT)
	})
	if err != nil {
		err = fmt.Errorf("error getting option on socket: %v", err)
		return
	}

	return
}

View on GitHub (pinned to 03255a9237)

Solutions

  1. Check whether the connection is still open before retrying — this error often surfaces during shutdown when fds are reclaimed
  2. Upgrade to a Linux kernel >= 2.6.37 that supports TCP_USER_TIMEOUT
  3. If seeing this in logs only, it is likely benign noise from a connection teardown race — verify gRPC keepalive behavior is functioning via higher-level connectivity callbacks
  4. If reproducing consistently on a custom dialer, ensure the dialer returns a standard *net.TCPConn

Example fix

// before: custom dialer returns a wrapped connection
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
    return wrappedConn{...}, nil
})
// after: ensure a standard *net.TCPConn is returned so GetTCPUserTimeout works
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
    d := net.Dialer{}
    return d.DialContext(ctx, "tcp", addr)
})
Defensive patterns

Strategy: validation

Validate before calling

// Before calling APIs that depend on TCP socket options, verify the connection is a live TCP conn:
func isLiveTCPConn(conn net.Conn) bool {
    tc, ok := conn.(*net.TCPConn)
    if !ok {
        return false
    }
    // Attempt SyscallConn to verify fd is valid
    raw, err := tc.SyscallConn()
    if err != nil {
        return false
    }
    _ = raw // fd is valid if no error
    return true
}

Try / catch

// GetTCPUserTimeout is internal; handle the caller gracefully:
if timeout, err := syscall.GetTCPUserTimeout(conn); err != nil {
    // Connection may be closed or socket unsupported; fall back to default keepalive
    logger.Warningf("could not read TCP_USER_TIMEOUT: %v, using default keepalive", err)
} else {
    // use timeout
}

Prevention

When it happens

Trigger: Called internally by gRPC on a *net.TCPConn when it needs to read the current TCP_USER_TIMEOUT value (e.g., during connection health checks or keepalive log dumps). Fires when rawConn.Control() returns a non-nil error — typically EBADF (closed fd), EINVAL, or a similar kernel-level errno on the underlying socket file descriptor.

Common situations: The connection has been closed or its file descriptor invalidated before gRPC attempts to read the option; running on a kernel that does not support TCP_USER_TIMEOUT (older Linux < 2.6.37); the conn passed is not a real TCP socket despite passing the earlier type assertion; connection races during shutdown where the fd is reclaimed between SyscallConn() and Control().

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/f0a4b5bf639ed750. Report an issue: GitHub.