diff --git a/src/NATS.Server/Routes/RouteConnection.cs b/src/NATS.Server/Routes/RouteConnection.cs
index c518867..e8e97ab 100644
--- a/src/NATS.Server/Routes/RouteConnection.cs
+++ b/src/NATS.Server/Routes/RouteConnection.cs
@@ -22,6 +22,36 @@ public sealed class RouteConnection(Socket socket) : IAsyncDisposable
/// for a given account. See .
///
public int PoolIndex { get; set; }
+
+ ///
+ /// The pool size agreed upon during handshake negotiation with the remote peer.
+ /// Defaults to 0 (no pooling / pre-negotiation state). Set after handshake completes.
+ /// Go reference: server/route.go negotiateRoutePool.
+ ///
+ public int NegotiatedPoolSize { get; private set; }
+
+ ///
+ /// Negotiates the effective route pool size between local and remote peers.
+ /// Returns Math.Min(localPoolSize, remotePoolSize), but returns 0 if
+ /// either side is 0 for backward compatibility with peers that do not support pooling.
+ /// Go reference: server/route.go negotiateRoutePool.
+ ///
+ public static int NegotiatePoolSize(int localPoolSize, int remotePoolSize)
+ {
+ if (localPoolSize == 0 || remotePoolSize == 0)
+ return 0;
+
+ return Math.Min(localPoolSize, remotePoolSize);
+ }
+
+ ///
+ /// Applies the result of pool size negotiation to this connection.
+ ///
+ internal void SetNegotiatedPoolSize(int negotiatedPoolSize)
+ {
+ NegotiatedPoolSize = negotiatedPoolSize;
+ }
+
public Func? RemoteSubscriptionReceived { get; set; }
public Func? RoutedMessageReceived { get; set; }
diff --git a/tests/NATS.Server.Tests/Routes/PoolSizeNegotiationTests.cs b/tests/NATS.Server.Tests/Routes/PoolSizeNegotiationTests.cs
new file mode 100644
index 0000000..2f65daa
--- /dev/null
+++ b/tests/NATS.Server.Tests/Routes/PoolSizeNegotiationTests.cs
@@ -0,0 +1,210 @@
+// Reference: golang/nats-server/server/route.go — negotiateRoutePool (pooling handshake logic)
+// Tests for route pool size negotiation between local and remote peers.
+
+using System.Net;
+using System.Net.Sockets;
+using Microsoft.Extensions.Logging.Abstractions;
+using NATS.Server.Configuration;
+using NATS.Server.Routes;
+
+namespace NATS.Server.Tests.Routes;
+
+///
+/// Tests for route pool size negotiation (Gap 13.3).
+/// Covers RouteConnection.NegotiatePoolSize static method,
+/// RouteConnection.NegotiatedPoolSize default, RouteManager.ConfiguredPoolSize,
+/// and RouteManager.GetEffectivePoolSize.
+/// Go reference: server/route.go negotiateRoutePool.
+///
+public class PoolSizeNegotiationTests
+{
+ ///
+ /// Creates a connected socket pair so RouteConnection can build its NetworkStream.
+ /// Returns the RouteConnection (owns the server-side socket) and the client-side
+ /// socket, which the caller must dispose to avoid resource leaks.
+ ///
+ private static (RouteConnection Route, Socket ClientSocket) CreateConnectedRoute()
+ {
+ var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
+ listener.Listen(1);
+ var port = ((IPEndPoint)listener.LocalEndPoint!).Port;
+
+ var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ client.Connect(IPAddress.Loopback, port);
+ var server = listener.Accept();
+ listener.Dispose();
+
+ return (new RouteConnection(server), client);
+ }
+
+ // -----------------------------------------------------------------------
+ // RouteConnection.NegotiatePoolSize static method
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void NegotiatePoolSize_BothNonZero_ReturnsMin()
+ {
+ // Go reference: server/route.go negotiateRoutePool — min(local, remote)
+ var result = RouteConnection.NegotiatePoolSize(3, 5);
+ result.ShouldBe(3);
+ }
+
+ [Fact]
+ public void NegotiatePoolSize_Equal_ReturnsSame()
+ {
+ // Go reference: server/route.go negotiateRoutePool — equal sizes
+ var result = RouteConnection.NegotiatePoolSize(3, 3);
+ result.ShouldBe(3);
+ }
+
+ [Fact]
+ public void NegotiatePoolSize_LocalZero_ReturnsZero()
+ {
+ // Go reference: server/route.go negotiateRoutePool — backward compat:
+ // if either peer sends 0 (no pooling advertised), result is 0.
+ var result = RouteConnection.NegotiatePoolSize(0, 5);
+ result.ShouldBe(0);
+ }
+
+ [Fact]
+ public void NegotiatePoolSize_RemoteZero_ReturnsZero()
+ {
+ // Go reference: server/route.go negotiateRoutePool — backward compat:
+ // remote peer without pool support sends 0; result must be 0.
+ var result = RouteConnection.NegotiatePoolSize(3, 0);
+ result.ShouldBe(0);
+ }
+
+ [Fact]
+ public void NegotiatePoolSize_BothZero_ReturnsZero()
+ {
+ // Go reference: server/route.go negotiateRoutePool — both disabled
+ var result = RouteConnection.NegotiatePoolSize(0, 0);
+ result.ShouldBe(0);
+ }
+
+ [Fact]
+ public void NegotiatePoolSize_LocalLarger_ReturnsRemote()
+ {
+ // Go reference: server/route.go negotiateRoutePool — min(10, 3) = 3
+ var result = RouteConnection.NegotiatePoolSize(10, 3);
+ result.ShouldBe(3);
+ }
+
+ [Fact]
+ public void NegotiatePoolSize_RemoteLarger_ReturnsLocal()
+ {
+ // Go reference: server/route.go negotiateRoutePool — min(3, 10) = 3
+ var result = RouteConnection.NegotiatePoolSize(3, 10);
+ result.ShouldBe(3);
+ }
+
+ [Fact]
+ public void NegotiatePoolSize_OneIsOne_ReturnsOne()
+ {
+ // Go reference: server/route.go negotiateRoutePool — min(1, 5) = 1
+ var result = RouteConnection.NegotiatePoolSize(1, 5);
+ result.ShouldBe(1);
+ }
+
+ // -----------------------------------------------------------------------
+ // RouteConnection.NegotiatedPoolSize default value
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void NegotiatedPoolSize_Default_IsZero()
+ {
+ // A newly created RouteConnection (not yet handshaked) must report
+ // NegotiatedPoolSize == 0 to signal that negotiation has not occurred.
+ // Go reference: server/route.go — pool size is 0 until negotiateRoutePool runs.
+ var (conn, clientSocket) = CreateConnectedRoute();
+ try
+ {
+ conn.NegotiatedPoolSize.ShouldBe(0);
+ }
+ finally
+ {
+ clientSocket.Dispose();
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // RouteManager.ComputeRoutePoolIdx determinism (regression guard)
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ComputeRoutePoolIdx_Deterministic()
+ {
+ // The same account name must always map to the same pool index.
+ // Go reference: server/route.go computeRoutePoolIdx (FNV-1a 32-bit hash).
+ const int poolSize = 5;
+ const string account = "test-account";
+
+ var first = RouteManager.ComputeRoutePoolIdx(poolSize, account);
+ var second = RouteManager.ComputeRoutePoolIdx(poolSize, account);
+ var third = RouteManager.ComputeRoutePoolIdx(poolSize, account);
+
+ first.ShouldBe(second);
+ second.ShouldBe(third);
+ first.ShouldBeGreaterThanOrEqualTo(0);
+ first.ShouldBeLessThan(poolSize);
+ }
+
+ // -----------------------------------------------------------------------
+ // RouteManager.ConfiguredPoolSize and GetEffectivePoolSize
+ // -----------------------------------------------------------------------
+
+ private static RouteManager MakeManager(int poolSize = 3)
+ {
+ var opts = new ClusterOptions
+ {
+ Host = "127.0.0.1",
+ Port = 0,
+ PoolSize = poolSize,
+ };
+ return new RouteManager(
+ opts,
+ new ServerStats(),
+ Guid.NewGuid().ToString("N"),
+ _ => { },
+ _ => { },
+ NullLogger.Instance);
+ }
+
+ [Fact]
+ public void ConfiguredPoolSize_ReturnsOptionsPoolSize()
+ {
+ // RouteManager should expose the configured pool size from ClusterOptions.
+ // Go reference: server/route.go opts.Cluster.PoolSize default is 3.
+ var manager = MakeManager(poolSize: 5);
+ manager.ConfiguredPoolSize.ShouldBe(5);
+ }
+
+ [Fact]
+ public void ConfiguredPoolSize_DefaultThreeWhenOptionsIsZero()
+ {
+ // When ClusterOptions.PoolSize is 0 (not explicitly set), the effective
+ // configured pool size should fall back to 3 (Go's default).
+ // Go reference: server/route.go default pool size of 3.
+ var manager = MakeManager(poolSize: 0);
+ manager.ConfiguredPoolSize.ShouldBe(3);
+ }
+
+ [Fact]
+ public void GetEffectivePoolSize_NullRemoteId_ReturnsConfigured()
+ {
+ // With no remote server ID, GetEffectivePoolSize returns the configured pool size.
+ var manager = MakeManager(poolSize: 4);
+ manager.GetEffectivePoolSize(null).ShouldBe(4);
+ }
+
+ [Fact]
+ public void GetEffectivePoolSize_UnknownRemoteId_ReturnsConfigured()
+ {
+ // For a remote server ID that has no connected (and negotiated) route,
+ // GetEffectivePoolSize should return the configured pool size.
+ var manager = MakeManager(poolSize: 3);
+ manager.GetEffectivePoolSize("unknown-server-id").ShouldBe(3);
+ }
+}