Node.js SFTP: ssh2-sftp-client, the OpenSSH Client, and a Way Without Either
Node has no SFTP in its standard library, so every Node SFTP script depends on something. Here is working code for ssh2-sftp-client, including the host key check it does not do for you, the OpenSSH sftp binary run as a child process, and a third way where Files.com connects to the partner's server and your Node only talks HTTPS, plus keys in a container and what to do when a transfer fails with nobody watching.
Node.js SFTP jobs rarely begin as infrastructure. They begin as a small script: pull a partner's files overnight, push a report to a vendor, move a batch into an inbound folder. Then the script becomes a scheduled job, more partners arrive, keys expire, servers change, and eventually someone is trying to work out why Tuesday's file never showed up.
Node has no SFTP client in its standard library, so every version of that script depends on something, and there are three sensible choices: ssh2-sftp-client, the library behind most Node SFTP integrations; the OpenSSH sftp command, run from Node; and moving the SFTP connection out of the application entirely by letting Files.com make it.
Here is working code for each, along with what the short examples leave out: host key verification, secrets in a container, partial uploads, and failing loudly on a schedule.
ssh2-sftp-client, the library most scripts use
ssh2-sftp-client wraps the lower-level ssh2 package in a promise-based API. Install it:
npm install ssh2-sftp-client
A complete upload with a private key:
const fs = require("node:fs");
const Client = require("ssh2-sftp-client");
async function upload() {
const sftp = new Client();
try {
await sftp.connect({
host: "sftp.example.com",
port: 22,
username: "alice",
privateKey: fs.readFileSync("/etc/sftp/keys/brightway_ed25519"),
});
await sftp.put("/data/exports/orders.csv", "/inbound/orders.csv");
} finally {
await sftp.end();
}
}
upload().catch((error) => {
console.error("SFTP upload failed:", error.message);
process.exitCode = 1;
});
connect takes the host, port, username, and either a password or a privateKey, and privateKey is the key's contents, not its path; add passphrase if the key is encrypted. put(local, remote) uploads, get(remote, local) downloads, list(path) returns the directory entries, and end() closes the connection.
The finally block matters: if put throws, the session still closes instead of hanging open on the server. The catch at the bottom matters as much, because a nonzero exit code is how the scheduler or container platform learns to retry and alert.
Remote paths use forward slashes even when Node runs on Windows.
Downloading and listing:
const entries = await sftp.list("/outbound");
for (const entry of entries) console.log(entry.name);
await sftp.get("/outbound/report.csv", "/data/imports/report.csv");
For large files, fastPut and fastGet send several requests concurrently and are usually faster. Test them against the destination first, because not every SFTP server handles concurrent requests well.
The host key check ssh2 does not do for you
The example above has a serious gap: it never verifies the server's identity. OpenSSH refuses to connect to a server whose host key it has not seen or whose key has changed. ssh2 does not read known_hosts and does not check host keys unless you ask, so the script trusts whatever server answers at sftp.example.com. That is the gap a DNS change or a compromised network walks through.
Close it with hostHash and hostVerifier, which ssh2-sftp-client passes straight to ssh2. The verifier receives a hash of the server's key and returns whether to continue:
await sftp.connect({
host: "sftp.example.com",
username: "alice",
privateKey: fs.readFileSync("/etc/sftp/keys/brightway_ed25519"),
hostHash: "sha256",
hostVerifier: (hash) => hash === process.env.BRIGHTWAY_HOST_KEY_SHA256,
});
Get the expected fingerprint from the server's administrator, store it with your configuration, and let a mismatch fail the connection.
One detail to watch: ssh2 hands the verifier the hash in the format its hostHash setting produces, so store the value in that same format rather than pasting a differently formatted OpenSSH fingerprint.
A changed host key may be a routine server rebuild, or it may mean you are connected to the wrong machine. An unattended job stops rather than guesses.
Upload to a temporary name, then rename
If another process watches the inbound directory, uploading straight to the final filename creates a race: the consumer may open the file before the transfer finishes. Upload under a temporary name and rename it once the upload succeeds:
const finalPath = "/inbound/orders.csv";
const temporaryPath = `${finalPath}.uploading`;
await sftp.put("/data/exports/orders.csv", temporaryPath);
await sftp.rename(temporaryPath, finalPath);
On nearly every SFTP server a rename within the same directory is atomic, so the receiving process sees either no file or the complete file, never a half-written CSV. Use a unique temporary name when jobs can overlap, and decide how stale temporary files get cleaned up.
The sftp binary, driven from Node
For a simple job you may not need an npm SFTP library at all. OpenSSH's sftp client is on every Linux and macOS host and on current Windows, it reads known_hosts, it handles keys, and Node can run it as a child process, feeding the batch commands on standard input:
const { execFileSync } = require("node:child_process");
const commands = `
put /data/exports/orders.csv /inbound/orders.csv.uploading
rename /inbound/orders.csv.uploading /inbound/orders.csv
bye
`;
execFileSync(
"sftp",
[
"-b",
"-",
"-i",
"/etc/sftp/keys/brightway_ed25519",
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=yes",
"-o",
"UserKnownHostsFile=/etc/sftp/known_hosts",
"alice@sftp.example.com",
],
{ input: commands, stdio: ["pipe", "inherit", "inherit"] }
);
-b - reads the commands from standard input, -i selects the key, BatchMode=yes stops the client from ever waiting for a password prompt, StrictHostKeyChecking=yes rejects an unknown or changed host key, and UserKnownHostsFile points at the job's own known_hosts.
execFileSync throws on a nonzero exit code, so a failed transfer fails the Node process. You get OpenSSH's mature key and host verification with no npm dependency.
What you give up is logic inside the transfer: the batch language can put, get, rename, rm, and mkdir, and everything conditional happens in the Node around it. For a scheduled one-file transfer that is often exactly the right balance.
Populate known_hosts before deployment by verifying the fingerprint through a trusted channel; ssh-keyscan retrieves a key but proves nothing about whose server it is.
Let Files.com make the SFTP connection
Both approaches leave SFTP inside your Node process: a private key mounted into the runtime, a host key stored and updated per partner, an outbound port-22 rule, connection configuration for every partner, and retries, alerts, and audit records you build yourself. The third option takes SFTP out of the process. Files.com connects to the partner's SFTP server on the cloud side, and your Node only talks to Files.com.
The mechanism is a Remote Server Mount. You give Files.com the partner's SFTP hostname, port, and credentials, and it mounts that server onto an empty folder on your site, say /partners/brightway. The folder is a live window onto the partner's server: an upload into it lands on the partner's machine in real time, and every list, rename, delete, or subfolder passes straight through. Files.com keeps no copy.
Files.com manages the keys and the host key
Adding the mount is a form, not code. Files.com authenticates to the partner with a password, a private key, or both, and it can generate the key pair itself (RSA, 4096-bit) so you only ever send the partner a public key; the private half never leaves Files.com. An existing key can be supplied in OpenSSH, PuTTY, or SSH2 format, and every credential is stored encrypted.
The host key check from the first section is on by default. Files.com records the partner's key on the first connection or takes the fingerprint the partner supplied, and if the key later changes the connection is disabled until an administrator approves the new one. The partner's network policy gets simpler too: they allowlist Files.com's published IP addresses instead of the changing egress addresses of every workload that might send a file.
Your Node talks HTTPS
With the mount in place, the Node side uses the Files.com JavaScript SDK over HTTPS on port 443 with an API key:
npm install files.com
import Files from "files.com/lib/Files.js";
import File from "files.com/lib/models/File.js";
Files.setApiKey(process.env.FILES_API_KEY);
await File.uploadFile(
"/partners/brightway/inbound/orders.csv",
"/data/exports/orders.csv"
);
const report = await File.download("/partners/brightway/outbound/report.csv");
await report.downloadToFile("/data/imports/report.csv");
uploadFile takes the destination path first and the local path second. File.download returns the file object, and downloadToFile writes its contents to disk.
The upload travels to Files.com over HTTPS, and Files.com writes it to the partner's SFTP server through the mount as it arrives. The application no longer needs an SFTP library, the partner's private key, the partner's fingerprint, outbound access on port 22, or any partner-specific connection logic.
The SDK retries and resumes interrupted transfers, and every operation lands in the Files.com audit log showing who moved which file and when. If your site runs on a custom domain, call Files.setBaseUrl once.
You may not need Node at all
Once the partner's server is a folder on Files.com, some scheduled jobs disappear rather than get rewritten. A Sync pushes to or pulls from the remote on a schedule, and an Automation renames, moves, copies, or routes files when they arrive.
The documented pattern is a partner that uploads invoices to its own SFTP server: Files.com pulls them every ten minutes, and a Move Files automation moves each one into the right processing folder.
Node handles only the logic that is genuinely unique to your company. Removing the transfer plumbing from the application is usually easier to operate than improving the plumbing.
Existing ssh2 code keeps working too
Files.com also accepts inbound SFTP on port 22 with SSH key authentication. Keep the ssh2-sftp-client code from the first section, point it at yourcompany.files.com, put into /partners/brightway/inbound, and Files.com passes the operation through to the partner's mounted server.
Your code keeps one stable credential and one host fingerprint, while partner-specific credentials and server details move into Files.com, which makes this a useful migration path.
Ten partners are ten mounts and one code path, and a partner whose SFTP server sits inside a private network is reachable through the Files.com Agent, which connects outbound with no inbound firewall rule.
Keys, retries, and silence in a container
Making the connection is the easy part. Never commit a password, API key, private key, or passphrase to source control or bake one into a container image; inject them as secrets at run time, as the process.env lines above do, mount the private key read-only, and limit its permissions to the job's user.
Make retries safe: schedulers retry failed jobs, and a transfer without the temporary-name-then-rename pattern can turn one failure into three duplicate deliveries.
Put a timeout around the job, because a transfer that stalls without failing is harder to notice than one that exits with an error.
And alert on silence as well as failure: a nonzero exit code is the minimum, and the worst file-transfer failures are jobs that report no error because they never ran, or files that never arrived because the partner never sent them.
Which one to use
ssh2-sftp-client when the transfer is part of your Node application's logic against any SFTP server, with hostVerifier set every time.
The OpenSSH sftp binary for a small, predictable batch job where zero dependencies and OpenSSH's own host key handling are worth more than logic inside the transfer.
A Files.com mount when you want credentials, host keys, retries, audit logs, and partner connections managed outside the application, when the far end is already Files.com, or when partner count has outgrown a folder of scripts.
If Node is not the language for the job, the same three ways are covered for Python, PowerShell, Java, and C#, and the SFTP automation guide covers bash and cron.
When scripts stop being the right answer
A handful of SFTP jobs is manageable. Dozens of scripts spread across repositories, schedulers, containers, and owners are not.
The signs are familiar: nobody knows which job owns a partner connection, several services hold copies of the same private key, changing one hostname takes multiple deployments, retries behave differently in every script, and a transfer only gets noticed when someone asks where the file is.
At that point the question is no longer how to send a file with Node. It is who operates the file-transfer system.
Files.com is the cloud-native File Orchestration Platform: one platform that replaces the stack of legacy tools IT teams run to move files, including the SFTP servers, the scripts that feed them, and the schedulers holding it all together. It speaks every protocol, connects 50+ cloud and on-prem systems, automates every transfer, and keeps a complete audit trail.
For the job in this post, that means the scheduled upload becomes a Files.com automation with retries and a delivery record, and Node stays for the logic that is genuinely yours, calling the SDK. If the SFTP server has to stay in your own data center, the ExaVault appliance is a free SFTP server for up to 50 users that every example above can target.
Start a free Files.com trial, create a mount, and point the SDK at it. No credit card required, and an SFTP endpoint with SSH key support is live in minutes.
Frequently asked questions
Does Node.js have built-in SFTP support?
No. Node's standard library has no SFTP client. Install ssh2-sftp-client (which wraps ssh2), run the OpenSSH sftp command as a child process, or use the Files.com SDK over HTTPS and let Files.com make the SFTP connection.
How do I upload a file over SFTP in Node.js?
With ssh2-sftp-client, await sftp.connect({...}) with a hostVerifier, await sftp.put(localPath, remotePath), and await sftp.end() in a finally block, setting a nonzero exit code on failure. Upload to a temporary name and rename it when another process watches the directory. With Files.com, await File.uploadFile(remotePath, localPath) over HTTPS, and a Remote Server Mount carries the file on to the partner's SFTP server.
Does ssh2 verify host keys?
Not by default. ssh2 does not read OpenSSH's known_hosts. Set hostHash and pass a hostVerifier function in the connect options that compares the server's key hash to the fingerprint the administrator gave you, and let a mismatch fail the connection.
How do I use an SSH key with ssh2-sftp-client?
Pass privateKey: fs.readFileSync(path) in the connect options, with passphrase if the key is encrypted. The value is the key's contents, not its path.
Can Files.com connect to my partner's SFTP server for me?
Yes. A Remote Server Mount connects a folder on your Files.com site to any SFTP server in real time. Files.com holds the credentials, can generate the key pair, pins the partner's host key, and passes every operation through. Your Node uploads to Files.com over HTTPS with the SDK, or a scheduled Sync moves the files with no code at all.