Ed25519 vs RSA on a Small VPS: Where SSH Handshake CPU Actually Goes

A TLS 1.3 handshake with an Ed25519 certificate costs about 11 µs of CPU on a modern server core. RSA-2048 signing costs roughly 90 µs, plus 35 µs for the client-side verify. Those numbers sound like an 8x win, and they are – right up until you measure a real server at load and find that neither signature operation appears anywhere near the top of the profile. This article measures all three layers: SSH host keys, TLS 1.3 cipher suites, and where the CPU actually goes at 5,000 handshakes per second.

The Key Exchange Is Not the Signature

The most common conflation in tuning discussions is between the host key algorithm and the key exchange method. On SSH, ssh-ed25519 and rsa-sha2-256 are host key types – what the server signs the exchange hash with. Separately, curve25519-sha256 and diffie-hellman-group14-sha256 are the key exchange algorithms that produce the shared secret. A server can run an RSA host key with Curve25519 key exchange, a combination far cheaper than the host key name suggests. Check what is negotiated, not what is configured.

# What the client and server settle on
ssh -vvv user@host 2>&1 | grep -E 'kex: algorithm|host key algorithm|peer server KEXINIT'

# Server side, once connected
sshd -T | grep -E 'hostkeyalgorithms|kexalgorithms|pubkeyacceptedalgorithms'

# Generate an Ed25519 host key alongside the RSA one
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ''
# sshd prefers the first matching key by default for new clients

Key exchange dominates the asymmetric cost of an SSH handshake, not the signature. Curve25519 scalar multiplication is around 50 µs per side on a small core; a group14 (2048-bit MODP) DH operation is several hundred microseconds to over a millisecond depending on the exponent size. Switching the host key from RSA to Ed25519 saves the signature – tens of microseconds – while switching the kex from DH group14 to Curve25519 saves an order of magnitude more. Tuning only the host key is the usual mistake.

Measured: SSH Handshake Cost Per Connection

Benchmark with the server actually accepting connections, not with a microbenchmark of openssl speed, because the syscall and process cost of sshd dwarfs the crypto. Use hyperfine against a throwaway host on the same LAN, or measure server-side CPU with perf stat around a fixed number of serial connections.

# 200 serial full handshakes, server CPU only (run on the server)
perf stat -e task-clock,cycles -p $(pgrep -o sshd) -- \
  bash -c 'for i in $(seq 200); do ssh -o BatchMode=yes host true; done'

# Client-side wall clock, RSA host key vs Ed25519
hyperfine --warmup 5 'ssh -o HostKeyAlgorithms=rsa-sha2-256 host true' \
                    'ssh -o HostKeyAlgorithms=ssh-ed25519 host true'

# Is the server forking per connection? (it should not be the bottleneck)
sshd -T | grep -E 'maxstartups|maxsessions|usepam'

On a 2 vCPU instance the typical result is 1.8-2.4 ms of server CPU per full SSH handshake, of which the asymmetric crypto is 100-200 µs. The remainder is process setup, PAM, and filesystem lookups for authorized_keys and sshd_config. Replacing the host key changes the 100 µs slice; it does not change the 2 ms. If SSH login latency is your problem, UsePAM no on a key-only host and a smaller authorized_keys file matter more than the curve.

TLS 1.3: Cipher Suite Choice and the Real Cost

In TLS 1.3 the handshake is one round trip and the key exchange is always ephemeral. The suite name encodes only the AEAD and hash; the key exchange group is negotiated separately. The “ECDHE vs RSA” question no longer exists in 1.3, so the real choices are the group (X25519, P-256, X448) and the AEAD.

OperationApprox CPU costWhere it runs
Ed25519 sign / verify11 µs / 28 µsServer / client
RSA-2048 sign / verify90 µs / 35 µsServer / client
X25519 key exchange45 µs per sideBoth
P-256 ECDHE85 µs per sideBoth
AES-128-GCM, 1 KiB record0.35 µsBoth, with AES-NI
ChaCha20-Poly1305, 1 KiB0.55 µsBoth, no AES-NI needed

The table makes the trade explicit. On a VPS whose CPU exposes AES-NI – every current x86 instance does – AES-GCM beats ChaCha20 for bulk data, and handshake cost is dominated by X25519, not the certificate type. Certificate type only matters when you sign on every connection, which in TLS 1.3 with resumption you do not.

Where the Bottleneck Really Is at Load

At 5,000 new TLS handshakes per second a 4 vCPU instance is already saturated, and the profile is not dominated by signature verification. Use perf top during a load test that forces full handshakes – resumption must be disabled or you measure a different path.

# Force full handshakes: new connection per request, no session reuse
wrk -t4 -c400 -d30s --latency -H 'Connection: close' https://host/

# While that runs:
perf top -C 0 --no-children
perf record -F 999 -a -- sleep 20 && perf report --sort symbol | head -30

# Kernel-side per-connection state
ss -s
nstat -az | grep -E 'TcpActiveOpens|TcpExtTCPReqQFullDrop|ListenOverflows'

# TLS-level counters if using nginx
nginx -T | grep -E 'ssl_protocols|ssl_ciphers|ssl_session' 

The consistent finding is that at high handshake rates the top entries are socket allocation, tcp_v4_connect/tcp_conn_request, and the kernel’s per-connection slab allocations – not crypto_* symbols. The handshake CPU is real but it is spread across two sides and amortised by resumption, while the connection setup cost is paid in full for every new connection. At 5k handshakes per second, RSA-2048 signing costs roughly 45% of a core; X25519 plus connection setup costs several times that. The signature choice is a second-order effect.

Configuring Both Properly

The right configuration is not one algorithm but a correct ordering: modern kex first, a modern host key or certificate, and session resumption enabled so that only the first connection pays the full asymmetric cost. On SSH, disable the legacy algorithms so a client cannot negotiate DH group1 or ssh-rsa (SHA-1) and pull you back into the expensive path by accident.

# /etc/ssh/sshd_config.d/10-hardening.conf
HostKey /etc/ssh/ssh_host_ed25519_key
KexAlgorithms curve25519-sha256,[email protected]
HostKeyAlgorithms ssh-ed25519
Ciphers [email protected],[email protected]
MACs [email protected]

# nginx: TLS 1.3 only for the modern path, resumption on
ssl_protocols TLSv1.2 TLSv1.3;
ssl_conf_command Options -SessionTicket;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;

Session ticket keys must be shared across workers: if they are not, a client resuming against a different worker completes a full handshake and your measured resumption rate will not match production. With OpenSSL 1.1.1 and later they are shared when ssl_session_cache is shared; verify with openssl s_client -reconnect and look for Reused.

The Conclusion the Numbers Support

Switch to Ed25519 host keys: small keys, no timing side channels, and a tenth of RSA’s signing cost – but do not expect it to rescue a server at 5,000 connections per second. In TLS, prefer X25519 and AES-128-GCM with AES-NI available, and treat certificate algorithm as a compliance and latency question rather than a throughput one. On a budget shared plan none of this is tunable; the difference between shared hosting and a VPS is precisely who owns the TLS stack. Above that line, choosing an instance with dedicated vCPUs matters more than any cipher list, because at 5k handshakes per second you are buying cores, not algorithms.

Leave a Reply