|
| 1 | +# Class: RoundRobinPool |
| 2 | + |
| 3 | +Extends: `undici.Dispatcher` |
| 4 | + |
| 5 | +A pool of [Client](/docs/docs/api/Client.md) instances connected to the same upstream target with round-robin client selection. |
| 6 | + |
| 7 | +Unlike [`Pool`](/docs/docs/api/Pool.md), which always selects the first available client, `RoundRobinPool` cycles through clients in a round-robin fashion. This ensures even distribution of requests across all connections, which is particularly useful when the upstream target is behind a load balancer that round-robins TCP connections across multiple backend servers (e.g., Kubernetes Services). |
| 8 | + |
| 9 | +Requests are not guaranteed to be dispatched in order of invocation. |
| 10 | + |
| 11 | +## `new RoundRobinPool(url[, options])` |
| 12 | + |
| 13 | +Arguments: |
| 14 | + |
| 15 | +* **url** `URL | string` - It should only include the **protocol, hostname, and port**. |
| 16 | +* **options** `RoundRobinPoolOptions` (optional) |
| 17 | + |
| 18 | +### Parameter: `RoundRobinPoolOptions` |
| 19 | + |
| 20 | +Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions) |
| 21 | + |
| 22 | +* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)` |
| 23 | +* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `RoundRobinPool` instance will create an unlimited amount of `Client` instances. |
| 24 | +* **clientTtl** `number | null` (optional) - Default: `null` - The amount of time before a `Client` instance is removed from the `RoundRobinPool` and closed. When set to `null`, `Client` instances will not be removed or closed based on age. |
| 25 | + |
| 26 | +## Use Case |
| 27 | + |
| 28 | +`RoundRobinPool` is designed for scenarios where: |
| 29 | + |
| 30 | +1. You connect to a single origin (e.g., `http://my-service.namespace.svc`) |
| 31 | +2. That origin is backed by a load balancer distributing TCP connections across multiple servers |
| 32 | +3. You want requests evenly distributed across all backend servers |
| 33 | + |
| 34 | +**Example**: In Kubernetes, when using a Service DNS name with multiple Pod replicas, kube-proxy load balances TCP connections. `RoundRobinPool` ensures each connection (and thus each Pod) receives an equal share of requests. |
| 35 | + |
| 36 | +### Important: Backend Distribution Considerations |
| 37 | + |
| 38 | +`RoundRobinPool` distributes **HTTP requests** evenly across **TCP connections**. Whether this translates to even backend server distribution depends on the load balancer's behavior: |
| 39 | + |
| 40 | +**✓ Works when the load balancer**: |
| 41 | +- Assigns different backends to different TCP connections from the same client |
| 42 | +- Uses algorithms like: round-robin, random, least-connections (without client affinity) |
| 43 | +- Example: Default Kubernetes Services without `sessionAffinity` |
| 44 | + |
| 45 | +**✗ Does NOT work when**: |
| 46 | +- Load balancer has client/source IP affinity (all connections from one IP → same backend) |
| 47 | +- Load balancer uses source-IP-hash or sticky sessions |
| 48 | + |
| 49 | +**How it works:** |
| 50 | +1. `RoundRobinPool` creates N TCP connections to the load balancer endpoint |
| 51 | +2. Load balancer assigns each TCP connection to a backend (per its algorithm) |
| 52 | +3. `RoundRobinPool` cycles HTTP requests across those N connections |
| 53 | +4. Result: Requests distributed proportionally to how the LB distributed the connections |
| 54 | + |
| 55 | +If the load balancer assigns all connections to the same backend (e.g., due to session affinity), `RoundRobinPool` cannot overcome this. In such cases, consider using [`BalancedPool`](/docs/docs/api/BalancedPool.md) with direct backend addresses (e.g., individual pod IPs) instead of a load-balanced endpoint. |
| 56 | + |
| 57 | +## Instance Properties |
| 58 | + |
| 59 | +### `RoundRobinPool.closed` |
| 60 | + |
| 61 | +Implements [Client.closed](/docs/docs/api/Client.md#clientclosed) |
| 62 | + |
| 63 | +### `RoundRobinPool.destroyed` |
| 64 | + |
| 65 | +Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed) |
| 66 | + |
| 67 | +### `RoundRobinPool.stats` |
| 68 | + |
| 69 | +Returns [`PoolStats`](PoolStats.md) instance for this pool. |
| 70 | + |
| 71 | +## Instance Methods |
| 72 | + |
| 73 | +### `RoundRobinPool.close([callback])` |
| 74 | + |
| 75 | +Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise). |
| 76 | + |
| 77 | +### `RoundRobinPool.destroy([error, callback])` |
| 78 | + |
| 79 | +Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise). |
| 80 | + |
| 81 | +### `RoundRobinPool.connect(options[, callback])` |
| 82 | + |
| 83 | +See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback). |
| 84 | + |
| 85 | +### `RoundRobinPool.dispatch(options, handler)` |
| 86 | + |
| 87 | +Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). |
| 88 | + |
| 89 | +### `RoundRobinPool.pipeline(options, handler)` |
| 90 | + |
| 91 | +See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler). |
| 92 | + |
| 93 | +### `RoundRobinPool.request(options[, callback])` |
| 94 | + |
| 95 | +See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). |
| 96 | + |
| 97 | +### `RoundRobinPool.stream(options, factory[, callback])` |
| 98 | + |
| 99 | +See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback). |
| 100 | + |
| 101 | +### `RoundRobinPool.upgrade(options[, callback])` |
| 102 | + |
| 103 | +See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback). |
| 104 | + |
| 105 | +## Instance Events |
| 106 | + |
| 107 | +### Event: `'connect'` |
| 108 | + |
| 109 | +See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect). |
| 110 | + |
| 111 | +### Event: `'disconnect'` |
| 112 | + |
| 113 | +See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect). |
| 114 | + |
| 115 | +### Event: `'drain'` |
| 116 | + |
| 117 | +See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain). |
| 118 | + |
| 119 | +## Example |
| 120 | + |
| 121 | +```javascript |
| 122 | +import { RoundRobinPool } from 'undici' |
| 123 | + |
| 124 | +const pool = new RoundRobinPool('http://my-service.default.svc.cluster.local', { |
| 125 | + connections: 10 |
| 126 | +}) |
| 127 | + |
| 128 | +// Requests will be distributed evenly across all 10 connections |
| 129 | +for (let i = 0; i < 100; i++) { |
| 130 | + const { body } = await pool.request({ |
| 131 | + path: '/api/data', |
| 132 | + method: 'GET' |
| 133 | + }) |
| 134 | + console.log(await body.json()) |
| 135 | +} |
| 136 | + |
| 137 | +await pool.close() |
| 138 | +``` |
| 139 | + |
| 140 | +## See Also |
| 141 | + |
| 142 | +- [Pool](/docs/docs/api/Pool.md) - Connection pool without round-robin |
| 143 | +- [BalancedPool](/docs/docs/api/BalancedPool.md) - Load balancing across multiple origins |
| 144 | +- [Issue #3648](https://github.com/nodejs/undici/issues/3648) - Original issue describing uneven distribution |
| 145 | + |
0 commit comments