Disclosure: the numbers below are from Tessera, which I work on. The reasoning applies to any proxy that sits in a session path.
Every vendor page in this category says something like "scales to thousands of users". It is a useless number, because a user who is not connected costs nothing. What costs something is a session that is open right now.
So here is the arithmetic instead, with the method, so you can check it against whatever you are evaluating.
The controller does not know how many engineers you employ and does not care how many targets are registered. It knows how many sessions are open.
The planning rule that has held up for us: on a normal working day, 10–20% of a team is connected at once. A 200-person engineering organisation is 20–40 concurrent sessions, not
This matters because the difference between the two numbers is the difference between a 512 MB VM and an argument about whether you need a cluster.
A proxied session is mostly buffers. In our case: about 256 KB of copy buffers, plus 6 to 10 goroutines at roughly 8 KB of stack each. Call it 320 KB per session. The base Go process is about 30 MB.
So 200 concurrent sessions is 200 × 320 KB ≈ 64 MB of live data, plus 30 MB base, ≈ 94 MB.
Except that is not what RSS
will show you, and this is the part people get wrong when they size Go services. Go does not hand memory back to the operating system promptly, and at the default GOGC=100
the collector lets the heap grow to roughly twice the live set before collecting. So resident memory settles at about double the arithmetic.
| Concurrent sessions | vCPU | Expected resident | Provision |
|---|---|---|---|
| up to 50 | 1 | ~90 MB | 512 MB |
| 50–200 | 2 | ~190 MB | 1 GB |
| 200+ | 4 | ~380 MB | 2 GB |
The gap between the last two columns is headroom for spikes, not a hidden cost. A controller serving 200 sessions really does use a couple of hundred megabytes. Our Helm chart ships requests: 256Mi
and limits: 1Gi
, which is the middle row.
If you are evaluating something else in this category, ask the vendor for per-session memory and the base process size. If they can only give you a user count, they have not measured it. The obvious worry is encryption. The proxy terminates TLS with the client and runs SSH with the target, so every byte is encrypted and decrypted twice.
On any CPU made in the last decade this is cheap, because AES-NI does it in hardware. For interactive work — people typing into terminals — CPU is essentially never the constraint. One vCPU handles 50 concurrent sessions; four handles 200-plus.
The exceptions are the two workloads that are not interactive: sustained file transfer, and RDP. Both of those hit a different limit first.
All session traffic passes through the controller twice. Client → controller → target and back. Moving N bytes costs 2N on the controller's network interface.
That interface is what saturates first, and it is what you should be sizing.
| Workload | Link |
|---|---|
| Interactive only (SSH · Kubernetes · SQL) | 100 Mbit/s |
Interactive + occasional scp / rsync |
|
| 1 Gbit/s | |
| Heavy transfer, or 50+ concurrent RDP | 10 Gbit/s |
Interactive terminal traffic is negligible — keystrokes and screen output are a few KB/s per session, so even 200 concurrent shells is single-digit Mbit/s. You could run that over a home connection.
RDP is a different category entirely. One RDP session runs 0.5 to 5 Mbit/s, more with video or a large high-colour desktop, and it is sustained rather than bursty. That is two to three orders of magnitude above a shell. Twenty concurrent RDP sessions, once you count the double hop, is 100–200 Mbit/s through the controller.
The practical consequence: if RDP is a meaningful part of your usage, size the link from the RDP sessions alone and treat everything else as rounding error. Mixing them into one average will give you a number that is wrong in the direction that hurts.
Bandwidth is the number people plan. Latency is the number people complain about.
The double hop adds round-trip time on every keystroke echo. If the controller is in a different region from the targets, users will describe the tool as "laggy" and they will be right, and no amount of bandwidth fixes it.
Put the controller in the same datacentre as the targets. If you have targets in three regions, that is an argument for three controllers, not for one very well-connected controller.
For completeness, the cost of the proxy itself. 20 concurrent SSH sessions over loopback:
| Native SSH | Through the proxy | |
|---|---|---|
| Aggregate throughput | ~540 MB/s | ~440 MB/s |
| Overhead | — | ~20% |
Loopback is deliberate. It removes the network from the measurement and leaves only the cost of the extra encrypt/decrypt pass and the buffer copies. On a real network the double hop adds its own cost, so 20% is a floor, not an expectation.
The honest conclusion from that number is not "20% is fine". It is: do not put bulk transfer through a session broker. Database dumps, backups, CI artefacts — route them outside. Use the broker for interactive access, which is what it is for and where the overhead is invisible.
Two separate growth curves.
Session recordings: only terminal sessions are recorded, as asciinema .cast
files, at roughly 1–5 MB per session-hour. Someone typing sits at the bottom of that range. tail -f
on a busy log, or htop
redrawing a full screen, goes past the top of it.
RDP is the exception again, in the opposite direction. The tunnel carries the RDP protocol rather than a terminal stream, so there is nothing to record — only audit events. That saves you the disk it would otherwise cost, and it costs you the replay. If your controls assume a recording exists for every session, that assumption does not hold for RDP.
The audit log lives in PostgreSQL and grows by events rather than by time — a session start and close, plus a row per command. A few hundred kilobytes per session-hour is a reasonable first guess, and your own first month is a much better one.
Both retention settings default to zero, which means keep forever. If you deploy and forget, the answer to "how big does this get" is "as big as the controller's entire history". Set them to your compliance window on day one, not on the day the disk fills.
The numbers here are from Tessera's requirements documentation and the benchmark is reproducible with the -compare mode of our loadtest tool: https://tessera.company/docs/reference/requirements/