Why your SFTP server should put files in your database (sometimes)

· ~9 minute read

Almost every SFTP server in the wild treats “the filesystem” as the place files go. You configure a home directory per user, the SSH daemon translates SFTP read/write commands into open(), read(), write() on real files, and the operating system handles the rest. It's the default model, it's been the default model for thirty years, and it works for most installations.

Some installations, it really doesn't. This post is about the cases where storing transferred files in a database instead of on a filesystem is the right call — what it buys you operationally, what it costs you in throughput, and what the design actually looks like when you build it. The short version: if your file transfer system has to play nicely with backup, replication, geographic redundancy, or transactional consumers downstream, a database storage backend earns its keep. If you're moving a million files a day between a vendor and a partner, just put them on a disk.

What "files on the filesystem" actually means in production

It looks simple from the SFTP server's perspective. From operations, it doesn't.

Your SFTP home directory lives on a volume. That volume needs:

None of these are deal-breakers. They're tractable problems with mature tools. But every one of them is a thing you have to design, monitor, and pay for separately. The filesystem is doing one job — storing bytes — and you're bolting four or five other systems on top to make those bytes safely usable.

What you get when you put files in a database instead

If the file content goes into a database table — chunked into reasonable rows, transactionally committed on close — then the bytes inherit everything the database already does for everything else in the schema. That's a meaningful list:

None of that is magic — it's just “files are data, treat them like data.” But the operational simplification is real. If your existing operations team is already great at running a database (and not great at running NFS or DRBD), you get to do file transfers in the half of your stack you already know.

What it costs

Three things, all manageable, all real.

Throughput, sometimes

A modern relational engine can absorb 200-300 MB/s of inserts on commodity hardware. That's faster than most SFTP clients can transmit. So for ordinary 1-100 MB transfers, the database backend keeps up with the wire. Where it starts to hurt is sustained multi-gigabyte transfers, because the database writes the bytes twice — once to the data file, once to the transaction log — and SQL engines aren't tuned the same way a filesystem is tuned for raw streaming throughput.

The mitigations: chunk the file into a row per N kilobytes (so the log records are bounded), use a relational engine with built-in page compression on the chunks table (cheap for text and XML payloads), and configure the database to use SIMPLE recovery mode if you don't need point-in-time recovery of the file content. With those three tweaks, a 10 GB upload to a database-backed SFTP server lands in roughly the same wall-clock time as the same upload to a local disk.

Operational footprint

The database becomes the system of record for file content, not just metadata. That means the database has to be sized for actual file volume, not just relational state. If your SFTP server moves a TB a month, the database is a TB a month bigger. This is a thing to plan for, not a thing to be surprised by.

Mental model shift for the team

People expect to be able to ls on the SFTP home directory and see real files. With a database backend, the “files” are rows. You still see them through the SFTP protocol, the Web Portal, WebDAV, FTP, whatever your clients use. But there is no ls /home/jsmith/inbound/ on the server's shell that shows them. The operations team has to be willing to query the database for “what's there.”

What we built

Stylus SFTP Server ships with a pluggable storage backend. The default is the local filesystem — the model everyone expects. The alternative is the JDBC backend, which writes file content into the relational database of your choice (H2, SQL Server, MySQL, or PostgreSQL).

The backend exposes a single interface, StorageBackend, with the operations you'd expect: openForRead, openForWrite, listFiles, exists, delete, rename. SFTP, FTP, FTPS, WebDAV, and the browser Portal all route through one of these backends. One implementation covers every protocol.

The JDBC implementation uses three tables:

sftp_file_storage   -- one row per file: path, name, owner, size, mtime
sftp_chunk_data     -- the actual bytes, chunked, ref_counted for dedup
sftp_file_chunks    -- which chunks make up which file (ordered)

The split into chunks lets identical chunks be deduplicated across files (the ref_count on sftp_chunk_data tracks how many files reference each chunk). The split also bounds the transaction log: instead of one transaction holding the whole file, each chunk write commits its own row, so the log can be truncated more aggressively. A long-running multi-gigabyte upload doesn't pin the entire log.

Configuration is one block in filesystem-config.xml:

<storage-backend>database</storage-backend>

<storage-database>
    <url>jdbc:sqlserver://localhost:1433;databaseName=sftpfiles</url>
    <username>sftp</username>
    <password>...</password>
    <max-pool-size>10</max-pool-size>
</storage-database>

That's it. Tables are auto-created on first boot. Every SFTP client, every web browser hitting the Portal, every WebDAV mount — they all transparently land in the database.

When you should not pick it

Be honest about your workload before turning it on.

The sweet spot

Database storage is the right call when files are transactional. When the file is the message: an EDI document, an invoice, a daily report, a CSV that drives a workflow. When you'd want to roll back the file alongside the database row that represents it. When the file's audit trail and the file's bytes need to live in the same backup window.

It's also the right call when you have a strong operational pattern around “the database is the source of truth.” If your team already runs a beautifully-tuned SQL Server Always On cluster, putting your inbound file exchange into the same cluster gives you DR for free. If you're already replicating Postgres to a second region for failover, your file transfers replicate too, no extra story.

And it's the right call when the alternative is filesystem-level features you don't actually want to operate: NFS exports, distributed filesystems, object-storage abstractions that introduce their own failure modes. Sometimes the simplest infrastructure decision is “use one fewer kind of system.”

The take-home

Default to the filesystem. The filesystem is fast, well-understood, and has a thirty-year tooling head start. If your operations team is happy with disk and a backup tape, don't change anything.

Reach for database storage when the operational story around files-as-files is the part that's hurting. When backup, replication, transactional consistency, or downstream querying are the actual pain. When you'd rather solve those problems once, in the part of your stack that already solves them for everything else.

The point of giving an SFTP server a pluggable storage backend isn't “database is better.” It's “put the bytes wherever fits your operations.”

Want to try the database backend?

Free evaluation key. Install on Windows or Linux, point at any JDBC database, and watch SFTP traffic land in your DB instead of your home volume.

Request Evaluation Key More articles ›