SSH Port Latency
Measure SSH-port route setup timing and an encrypted SSH service-request RTT directly from the Traversio library.
Traversio exposes a small public utility API for measuring how an SSH route behaves before final-target authentication.
Use SSHClient.measurePortLatency(...) when you want to compare:
- route setup time
- a real
SSH_MSG_SERVICE_REQUEST("ssh-userauth")/SSH_MSG_SERVICE_ACCEPTround trip after the final target's SSH transport is encrypted
This is useful for support screens, diagnostics exports, and server responsiveness checks before authentication on the final target.
This utility is not a host-trust or login-success check. Final-target samples verify the server's host-key signature as part of key exchange, then measure the encrypted ssh-userauth service request; they do not apply the final target's hostKeyPolicy and they stop before final-target user authentication. Use SSHClient.connect(...) when the app needs the normal host-trust decision.
For a machine that is already connected, do not run this utility on a recurring dashboard timer. Read await connection.latency from the existing SSHConnection instead. Traversio updates that snapshot from live channel/global requests and configured keepalive replies, so a connected dashboard can refresh its displayed value without repeatedly opening short-lived SSH routes.
API Shape
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, visionOS 1.0, *)
public static func measurePortLatency(
host: String,
port: UInt16 = 22,
options: SSHPortLatencyOptions = .init()
) async throws -> SSHPortLatencyReport
public static func measurePortLatency(
host: String,
port: UInt16 = 22,
connectionProxy: SSHConnectionProxy? = nil,
proxyJumpHosts: [SSHProxyJumpHost] = [],
options: SSHPortLatencyOptions = .init(),
logHandler: SSHClientLogHandler = .disabled
) async throws -> SSHPortLatencyReport
public static func measurePortLatency(
configuration: SSHClientConfiguration,
options: SSHPortLatencyOptions = .init(),
logHandler: SSHClientLogHandler = .disabled
) async throws -> SSHPortLatencyReportSupporting public types:
SSHPortLatencyOptionsSSHPortLatencyReportSSHPortLatencySampleSSHPortLatencyFailureSSHPortLatencyStatistics
Measure An Endpoint
import Traversio
@available(macOS 10.15, iOS 13.0, *)
func inspectPort() async throws {
let report = try await SSHClient.measurePortLatency(
host: "example.com",
port: 22,
options: SSHPortLatencyOptions(
sampleCount: 5,
connectTimeout: 2,
firstServerByteTimeout: 2,
delayBetweenSamples: 0.1
)
)
print(report.connectRTTStatistics.averageMilliseconds)
print(report.sshServiceRequestRTTStatistics.averageMilliseconds)
print(report.estimatedPathOneWayFromSSHServiceRequestStatistics.averageMilliseconds)
}Measure Through A Configured Route
Use the configuration: overload when your machine definition already carries connectionProxy or proxyJumpHosts.
let report = try await SSHClient.measurePortLatency(
configuration: machineConfiguration,
options: SSHPortLatencyOptions(
sampleCount: 5,
connectTimeout: 2,
firstServerByteTimeout: 2,
delayBetweenSamples: 0.1
)
)Route behavior:
- direct targets use Traversio's normal TCP transport factory
connectionProxytargets include SOCKS5 or HTTP CONNECT setup in each sampleproxyJumpHoststargets authenticate the jump chain once, then measure freshdirect-tcpipchannel opens to the final endpoint- each final-target sample completes SSH version exchange, key exchange, and
NEWKEYS, then measures the encryptedssh-userauthservice-request RTT - final-target samples do not apply the final target's host-trust policy; the route is measured before final-target user authentication
The report keeps both the raw timings and the aggregated statistics, so your app can either:
- show only the summary values
- expose every sample attempt
- keep the failures alongside the successful samples in a support export
The Main Metrics
The library keeps these values on purpose. They answer different questions.
report.connectRTTStatistics.averageMilliseconds
This is the average time from stream setup start until the route setup step returns.
Use it when you want to know:
- how fast the local machine can establish the selected route
- whether a nearby VPN, proxy, or load balancer is accepting the route quickly
- whether failures are happening before any server-side SSH bytes are seen
This is a route setup duration, not a replacement for ping. On some backends a direct stream handle can be returned before the peer has sent anything. For ProxyJump samples, this is the final direct-tcpip channel open after the jump chain is already authenticated.
report.sshServiceRequestRTTStatistics.averageMilliseconds
This is the average round-trip time for the final-target SSH service request.
The library first completes SSH version exchange, key exchange, and NEWKEYS with the final target. It then measures the time from sending SSH_MSG_SERVICE_REQUEST("ssh-userauth") to receiving SSH_MSG_SERVICE_ACCEPT.
Use it when you want a ping-like number for UI or support output. It is not ICMP ping, but it has the same round-trip shape.
report.estimatedPathOneWayFromSSHServiceRequestStatistics.averageMilliseconds
This is derived from:
report.sshServiceRequestRTTStatistics.averageMilliseconds / 2
The library first completes SSH version exchange, key exchange, and NEWKEYS with the final target. It then measures the round trip from sending SSH_MSG_SERVICE_REQUEST("ssh-userauth") to receiving SSH_MSG_SERVICE_ACCEPT, divides that value by 2, and exposes that number as an estimate.
Use it only when you explicitly want a derived one-way estimate:
- "is this endpoint really nearby, or did a local proxy only make
connect()look fast?" - "which server feels farther away when VPN, transparent proxying, or jump-host routing flattens route setup time?"
This remains an estimate because it includes small endpoint scheduling and packet-processing costs. In normal SSH servers those costs are usually small compared with proxy, jump-host, and wide-area network latency.
How To Choose
If you are deciding which metric to show in your own tooling, use this rule:
- show
report.sshServiceRequestRTTStatistics.averageMillisecondswhen you want a ping-like latency value - show
report.connectRTTStatistics.averageMillisecondswhen you care about route setup on the local path - show
report.estimatedPathOneWayFromSSHServiceRequestStatistics.averageMillisecondsonly when the label clearly says it is an estimated one-way value - show both when you need to explain why a quick TCP connect and a slower final-server SSH service response can coexist
In support and operations workflows, showing both is usually the most useful choice.
Why The Numbers Can Disagree
Route setup duration and SSH service-request RTT can diverge significantly, and that difference often reflects the network path accurately.
Common reasons:
- a VPN or transparent proxy accepts the TCP connection near the client, so route setup becomes very small
- the remote SSH server is still far away, so the encrypted service request takes longer to round-trip
- a proxy chain, load balancer, or middlebox finishes the TCP setup before the real server has replied
- a jump host opens the final channel quickly while the final SSH server still has farther to reach
- the server adds a small amount of scheduling or packet-processing delay
That means this kind of result is normal:
let report = try await SSHClient.measurePortLatency(host: "example.com")
print(report.connectRTTStatistics.averageMilliseconds)
print(report.sshServiceRequestRTTStatistics.averageMilliseconds)
print(report.estimatedPathOneWayFromSSHServiceRequestStatistics.averageMilliseconds)You might see a very small route setup value and a much larger SSH service-request RTT. That usually means the route setup completed near the client, and the real final-server round trip still had to travel farther.
Why This Uses Service Request
At the TCP layer, the earliest server response is SYN-ACK. Packet boundaries stay below the stream socket API.
At the SSH application layer, the first SSH-related data is the server identification line:
SSH-2.0-OpenSSH_9.9RFC 4253 allows the server to send non-SSH- text lines before that identification line. After identification exchange, the first binary SSH transport packet is SSH_MSG_KEXINIT.
Those early bytes are useful diagnostics, but they are not a clean request/response timing boundary. A server banner is sent proactively, and a server may also send SSH_MSG_KEXINIT immediately after its identification string. Dividing either timing by 2 can therefore overstate or understate a real path one-way estimate.
Traversio instead waits until the final target's encrypted SSH transport is active, then measures a concrete request/response pair:
SSH_MSG_SERVICE_REQUEST("ssh-userauth") -> SSH_MSG_SERVICE_ACCEPTThat packet pair does not authenticate. It just asks the server to enter the SSH user-authentication service. The server-side work is small, so the RTT is usually dominated by the selected route to the final SSH server and back.
Scope
This API covers a narrow diagnostic task:
- it opens a direct TCP socket, a configured connection-proxy stream, or a ProxyJump
direct-tcpipstream tohost:port - it can reuse route fields from
SSHClientConfiguration - it authenticates ProxyJump hosts because the final channel needs an established SSH hop chain
- it completes final-target SSH transport setup and stops after
SSH_MSG_SERVICE_ACCEPT
Use it as a latency utility. Full session success still depends on the later SSH stages.
Related Types
SSHPortLatencyOptions controls:
sampleCountconnectTimeoutfirstServerByteTimeoutdelayBetweenSamples
Invalid values do not need to be handled with process-level traps. Call
try options.validate() when you want to preflight user input, or pass the
options into SSHClient.measurePortLatency(...) and catch SSHPortLatencyError
from the measurement call. The typed cases cover invalid sample count, invalid
timeouts, route setup timeout, SSH service-request timeout, and no successful
samples.
SSHPortLatencyReport gives you:
samplesfailuresconnectRTTStatisticsfirstServerByteAfterConnectStatisticssshServiceRequestRTTStatisticsestimatedPathOneWayFromFirstServerByteStatisticsestimatedPathOneWayFromSSHServiceRequestStatistics
Recommended Default
For developer-facing diagnostics pages and support bundles:
- keep
report.connectRTTStatistics.averageMilliseconds - keep
report.sshServiceRequestRTTStatistics.averageMilliseconds - keep
report.estimatedPathOneWayFromSSHServiceRequestStatistics.averageMillisecondsonly when you clearly label it as an estimated one-way value
That gives developers enough context to choose the metric that matches their own environment and support workflow.
For the full type list and signatures, see Public API. For real connection failures, read Diagnostics.