Sharing One Connection
Run monitoring execs, an interactive shell, and SFTP on one SSH connection by giving each workflow its own channel.
The Protocol Rule
SSH does allow one authenticated connection to carry multiple channels at the same time.
The important detail is narrower than that:
shell,exec, andsubsystemare all requests on asessionchannel- only one of those requests can succeed on a single
sessionchannel
So the right mental model is:
- one
SSHConnection - many child channels
- one purpose per
SSHSession
That leads to this usage model:
- many monitoring commands in parallel should use many exec channels
- a terminal should live on its own shell channel
- SFTP should live on its own subsystem channel
Supported Shape
In practice:
- many
connection.execute(_:)andconnection.openExec(_:)calls can overlap on oneSSHConnection connection.openShell(...)can stay open while other exec or SFTP work runsconnection.openSFTP(...)opens a separate session channel on the same SSH connection
This is the shape you want for a server-monitoring client: keep one authenticated connection alive, then open separate channels for each job type.
Limits To Remember
- one
SSHSessionmaps to one channel role, so shell, exec, and SFTP each use their own channel - when multiple tasks write to one
SSHSessionstdin, serialize those writes in the application layer - one
SFTPClientcan now overlap multiple request/response pairs on one subsystem, and whole-file reads or writes can keep a bounded number of SFTP requests in flight on that same channel when you opt into the concurrency knobs
Example: Run Monitoring Commands In Parallel
For one-shot monitoring commands, execute(_:) is usually the cleanest API:
import Traversio
struct ServerSnapshot: Sendable {
let cpu: String
let memory: String
let disk: String
}
@available(macOS 26.0, iOS 26.0, *)
func collectSnapshot(on connection: SSHConnection) async throws -> ServerSnapshot {
async let cpuResult = connection.execute("top -bn1 | head -n 5")
async let memoryResult = connection.execute("free -m")
async let diskResult = connection.execute("df -h /")
let cpu = try await cpuResult
let memory = try await memoryResult
let disk = try await diskResult
return ServerSnapshot(
cpu: String(decoding: cpu.standardOutput, as: UTF8.self),
memory: String(decoding: memory.standardOutput, as: UTF8.self),
disk: String(decoding: disk.standardOutput, as: UTF8.self)
)
}This opens three separate exec channels on the same SSH connection and collects them independently.
Example: Keep Shell, SFTP, And Monitoring Together
If your app needs a terminal, background monitoring, and file transfer at the same time, keep one long-lived SSHConnection and open child wrappers from it:
import Traversio
@available(macOS 26.0, iOS 26.0, *)
func runWorkspace(configuration: SSHClientConfiguration) async throws {
let connection = try await SSHClient.connect(configuration: configuration)
let shell = try await connection.openShell()
let sftp = try await connection.openSFTP()
let monitorTask = Task {
while !Task.isCancelled {
let snapshot = try await collectSnapshot(on: connection)
print(snapshot.cpu)
try await Task.sleep(nanoseconds: 5_000_000_000)
}
}
do {
let version = try await sftp.currentVersionExchange()
print("SFTP version:", version.serverVersion)
try await shell.write("uname -a\n")
try await shell.write("uptime\n")
let uploadTarget = "/tmp/diagnostics.txt"
try await sftp.writeFile(uploadTarget, data: Array("monitor ready\n".utf8))
} catch {
monitorTask.cancel()
try? await shell.close()
try? await sftp.close()
await connection.close()
throw error
}
monitorTask.cancel()
try await shell.close()
try await sftp.close()
await connection.close()
}The important part is the ownership shape:
connectionowns the real SSH connectionshellowns one shell channelsftpowns one subsystem channel- each monitoring sample opens its own exec channel
When To Open More Than One SFTP Client
A single SFTPClient can overlap independent metadata and handle requests on one subsystem.
If you later need:
- one long upload while
- another task lists directories or reads a file
then opening another SFTPClient from the same SSHConnection remains a good shape. Each subsystem channel gets its own flow control, and one long-running transfer can still dominate the window on a single client.