The pipelining test only proved requests still complete under the cap. It did not show the peer being slowed, which is the whole claim: with the cap at one and every handler parked, the client's send() blocks on the channel window and finishes once the handlers are released. Raising the cap to 64 makes the test fail, so the stall comes from the cap rather than the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@webnet/sftp
SFTP (protocol version 3) client and server built on @webnet/ssh and @webnet/vfs.
SFTPClient implements AsyncVFS (stat, readdir, readFile/readFileRange, writeFile, delete, mkdir, move) over an SFTP session. It can be created three ways: SFTPClient.connect runs SFTP as a new subsystem channel on an SSHClientConnection the caller owns, SFTPClient.fromChannel runs it over a subsystem channel the caller already opened, or new SFTPClient({ dialer, host, user, ... }) dials its own SSH connection. SFTPServer drives a full SSH server (via @webnet/ssh) and serves an AsyncVFS over the SFTP subsystem to accepted connections, with optional per-login authenticate to select a different VFS per credential; serveSFTPChannel runs the SFTP server protocol directly over a Channel for callers who already have an authenticated SSH connection. Errors surface as SFTPError, and SSH_FX holds the SFTP status codes.
Entry points
| Entry point | Description |
|---|---|
@webnet/sftp |
SFTPClient, SFTPServer, serveSFTPChannel, SFTPError, SSH_FX, and their option types. |
@webnet/sftp/client |
SFTPClient and its option types only. |
@webnet/sftp/server |
SFTPServer, serveSFTPChannel, and their option types only. |
_internals entry points are unstable and are not part of the public API.
Limits
The server bounds what one session can be made to hold. SFTP_LIMITS holds the defaults; SFTPServerOptions and the third argument to serveSFTPChannel override them per session. The SSH connection underneath applies its own limits.
| What is bounded | Default | Option | When the peer exceeds it |
|---|---|---|---|
| Requests dispatched at once | 64 | maxInflightRequests |
The server stops reading until a reply goes out. |
| Handles open per session | 256 | maxOpenHandles |
SSH_FXP_OPEN and SSH_FXP_OPENDIR fail with SSH_FX_FAILURE. |
Bytes one SSH_FXP_READ returns |
256 KiB | maxReadLength |
The reply is clamped, which is a legal short read. |
| Bytes in one inbound packet | 1 MiB | — | The session fails. |
Pipelining is how SFTP clients are meant to reach full speed, so exceeding the in-flight cap throttles the peer through the SSH channel window instead of failing the session. The read cap matters because the packet-size limit bounds the request, not the reply it asks for: without it, a single 4 GiB SSH_FXP_READ is a 4 GiB allocation.
Usage
Client
import { SFTPClient } from "@webnet/sftp/client"
import type { RawDialer } from "@webnet/transport"
declare const dialer: RawDialer
const client = new SFTPClient({ dialer, host: "example.com", user: "u", password: "p" })
const entries = await client.readdir("/home/u")
const stream = await client.readFile("/home/u/file.txt")
await client.close()
Server
import { SFTPServer } from "@webnet/sftp/server"
import type { RawListener } from "@webnet/transport"
import type { AsyncVFS } from "@webnet/vfs"
declare const vfs: AsyncVFS
declare const listener: RawListener
const server = new SFTPServer({ vfs })
await server.listen(listener)
See also
@webnet/ssh— the SSH connections, channels, and subsystems this package runs over@webnet/vfs— theAsyncVFSinterface this package implements and serves@webnet/ftp— an alternative file-transfer protocol using FTP/FTPS instead of SSH