Performance Tuning SFTP Gateway
TLDR - Quick Summary
What: Tune SFTP Gateway for demanding workloads — many concurrent connections, large files, and busy automated clients.
Size this first: The JVM heap (
-Xmx). An undersized heap is the single most common cause of performance incidents.Config File: Most settings live in
/opt/sftpgw/application.properties; the heap lives in the service.conffile.Apply Changes:
service sftpgw-admin-api restart(briefly interrupts active transfers — use a maintenance window).Common misconception: The OpenSSH
MaxSessions/MaxStartups/MaxAuthTriesdirectives tune the admin shell on port 2222, not SFTP file transfer.
Go further: SFTP Gateway for the Enterprise
Running secure file transfer at enterprise scale? Our free white paper covers high-availability SFTP Gateway architecture, security and compliance, and cloud integration across AWS, Azure, and Google Cloud.
👉 Download the SFTP Gateway white paper — free; a short registration unlocks the PDF.
Overview
This guide covers the settings that affect SFTP Gateway throughput, connection capacity, and stability under load, in the order they usually matter. Most live in /opt/sftpgw/application.properties (see the Application Properties reference); the JVM heap lives in the service .conf file.
Before you start: which knob actually matters?
When an instance struggles under load, the cause is almost always one of these, in order of likelihood:
- The JVM heap is too small for the number of concurrent connections. This produces
java.lang.OutOfMemoryError: Java heap spaceand connection resets. Fix the heap first. - Connections are accumulating because clients open them and never close them cleanly. The idle timeout and heap headroom are your main defenses today.
- The cloud storage backend or network is the bottleneck, not the SFTP server itself.
⚠️ Common misconception: the operating-system SSH configuration does not control SFTP. On a deployed instance there are two separate SSH services. SFTP file transfer is the SFTP Gateway Java application on port 22. The operating-system admin shell is a separate OpenSSH service on port 2222. The OpenSSH directives
MaxSessions,MaxStartups, andMaxAuthTriestune only the admin shell on port 2222 and have no effect on SFTP capacity or speed.
Applying changes: edit /opt/sftpgw/application.properties (or JAVA_OPTS in the service .conf file) and run service sftpgw-admin-api restart in a maintenance window; the restart briefly interrupts active transfers.
Instance sizing: throughput is CPU-bound
Before tuning software settings, pick an instance that can move your data. SFTP Gateway streams every byte from the SFTP connection straight to S3 in memory, encrypting as it goes, so raw throughput is limited by CPU, not network bandwidth. Compute-optimized, current-generation instances deliver far more throughput per dollar than general-purpose or network-optimized ones.
Rules of thumb from our internal benchmarks (SFTP Gateway v3.7.4 and later):
- Upgrade first. v3.7.4+ is up to 3× faster than older releases through CPU-usage optimizations — a free throughput gain with no infrastructure change.
- Prefer the
c7a(compute-optimized) family. Ac7a.largecosts about the same as anm5.largebut performs up to twice as well. Avoid burstable T3 instances in production — they throttle under sustained load. - Throughput scales with vCPUs, with diminishing returns past the mid-range:
| Instance type | vCPUs | Approx. throughput |
|---|---|---|
c7a.large | 2 | 100 MB/s |
c7a.2xlarge | 8 | 256 MB/s |
c7a.4xlarge | 16 | 480 MB/s |
c7a.8xlarge | 32 | 546 MB/s |
c7a.16xlarge | 64 | 668 MB/s |
Note: these figures are approximate, from internal benchmarks (v3.7.4 and later) with concurrent transfers. Your results depend on file sizes, concurrency, client capacity, and the negotiated encryption algorithms.
One SFTP connection is a single stream and won't saturate an instance. Reaching these numbers requires several files or sessions transferring at once (which happens naturally with many users); for a single transfer, the client's own upload speed and disk are usually the limit.
Starting points: a single c7a.large (~100 MB/s) suits light, intermittent workloads. For sustained throughput or uptime-critical service, run two or more instances in high availability (see section 3, Scale out, not just up): roughly 200 MB/s from two c7a.large, 500 MB/s from two c7a.2xlarge, and 1 GB/s+ from two c7a.4xlarge or larger. At 1 GB/s+, the Network Load Balancer itself can throttle until its capacity units (LCUs) warm up — spread instances across as many availability zones as possible to reduce this. Encryption choice also affects speed — see Encryption Algorithms.
Then size the JVM heap to the instance you chose (next section).
1. JVM heap (size this first)
The heap is set with JVM options, not an application property. On the EC2 / AMI image it lives in the service .conf file (/opt/sftpgw/sftpgateway-admin-api-*.conf) as a JAVA_OPTS entry, for example JAVA_OPTS="-Xms12000m -Xmx44000m ...". On containers the heap is a percentage of the container memory limit (InitialRAMPercentage / MaxRAMPercentage), so the lever there is the container memory limit.
See Configuring VM Memory Settings for the step-by-step procedure and per-RAM
-Xmxguideline values.
Why it matters: each live SFTP connection holds a few megabytes of session and transport state before any file buffers. A few hundred connections plus transfer buffers can exhaust a small heap and trigger an OutOfMemoryError, which resets every connection on the instance at once.
- Leave memory for the OS and, on the AMI image, for the bundled PostgreSQL: typically about 2 GB for the OS plus the database's share, with the rest to
-Xmx. - Set
-Xmsto about 30 percent of-Xmx. - After resizing the instance, raise
-Xmxto match. Adding vCPU and RAM does nothing if the heap stays pinned to the old value. - 64 GB example with the bundled database:
-Xms12000m -Xmx44000m.
Note: Very large heaps do not scale forever. Garbage-collection pauses grow with heap size and can show up as intermittent, whole-instance slowness. If you keep reaching for an ever-larger single instance, scaling out (section 3) is usually the better answer.
2. High-volume scenarios (many concurrent connections)
The most effective application setting for connection load is the idle timeout, which returns the memory held by sessions that clients leave open.
| Property | Default | What it does |
|---|---|---|
sftp.connection-idle-timeout-seconds | 1200 (20 min) | Disconnects an idle authenticated connection, freeing its heap. Lower to 300 to reclaim abandoned sessions — the most effective, lowest-risk change for connection accumulation. |
sftp.auth-idle-timeout-seconds | 120 (2 min) | Drops a connection that does not finish authenticating in time. Protects the auth path from slow or stalled clients. |
This release has no per-connection or per-user ceiling, so the heap is the effective limit. Size it for peak concurrency; when one instance can no longer hold peak load comfortably, scale out instead of growing it.
3. Scale out, not just up (high availability)
For high-volume deployments, running several smaller instances behind a load balancer is usually a better choice than one very large instance. It raises capacity, removes the single point of failure, and avoids the long garbage-collection pauses that come with very large heaps.
| One large instance | Several instances behind a load balancer |
|---|---|
| One JVM heap is the hard ceiling on connections. | Capacity is the sum of all instances; add instances to grow. |
| A very large heap means longer GC pauses, felt as intermittent slowness. | Each instance keeps a modest heap with shorter pauses. |
| If it runs out of memory or restarts, every connection drops at once. | One instance failing drops only its share; the load balancer routes new connections to healthy instances. |
| Upgrades require a full outage. | Instances can be restarted or upgraded one at a time (rolling). |
Shared automatically across instances (all instances connect to the same external database): the SSH host key (so clients that pin it in their known-hosts file connect to any instance without a mismatch warning), user accounts, folders, cloud mounts, settings, failed-login and IP-ban state, the admin sign-in secret, and scheduled jobs. Enforced per instance by design: connection counts, rate limits, and the database pool — each instance protects its own heap.
Requirements:
- A shared external PostgreSQL (for example Amazon RDS), itself run in a highly-available configuration, rather than the per-instance database bundled in the image.
- A Layer 4 (TCP) load balancer — an AWS Network Load Balancer (NLB). No Layer 7 rules and no sticky sessions are needed: each SFTP connection is self-contained.
- Real client IP through the load balancer. On AWS with a Network Load Balancer this is automatic. (On other platforms, enable PROXY protocol on the load balancer and set
features.sftp-subsystem.load-balancer.enable-proxy-protocol=trueplusfeatures.sftp-subsystem.load-balancer.ip-addressesto the trusted load-balancer addresses.) - Standard provisioning, so each instance adopts the cluster's shared host key and sign-in secret from the database.
Note: Before committing to a multi-instance deployment, confirm your license covers running multiple instances. Contact Thorn Tech support if you are unsure. See also the High Availability setup guides.
4. Large file transfers
When SFTP Gateway receives a file it streams the bytes through to the configured cloud backend. The way each backend buffers data determines both transfer speed and how much heap each transfer consumes. These properties apply to the matching cloud provider only.
To raise the maximum single-file size (as opposed to throughput), see Uploading very large files (increasing the maximum upload size).
Amazon S3
| Property | Default | What it does and how to tune |
|---|---|---|
features.file-system.aws-s3.max-simple-upload-size-bytes | 8388608 (8 MB) | Files at or below this size upload in a single request; larger files use multipart upload, and this value is also the part size. For consistently large files, raising it (for example to 64 MB) reduces parts and round-trips. Larger parts use more heap per in-flight part, so raise the heap to match. |
features.file-system.aws-s3.max-inflight-upload-parts | 4 | How many upload parts may be in flight at once per transfer before the writer waits. Heap per transfer is roughly (in-flight parts) × (part size). Lower to 2 under very high concurrency to cut memory pressure. |
features.file-system.aws-s3.http-max-concurrency | 500 | Maximum concurrent HTTP connections in the S3 client pool. Raise if many simultaneous transfers saturate the pool and stall. |
features.file-system.aws-s3.metadata-timeout-seconds | 30 | Timeout for metadata operations (listing, stat, multipart setup). Increase for slow or distant regions. |
features.file-system.aws-s3.data-timeout-seconds | 300 | Timeout for each data operation (a simple upload, a single part, completion). Increase for very large parts or slow links. |
Memory math for large files on S3: in-flight memory per transfer is about
max-inflight-upload-parts× part size. With the defaults that is 4 × 8 MB = 32 MB per active large-file upload. Multiply by the number of simultaneous large uploads to estimate total heap demand, and confirm it fits inside-Xmx.
Local file system and other clouds
- Local file system streams to disk and is bounded by disk I/O.
An AWS instance can also hold cloud connections to other providers. Those backends have their own buffering model and properties, covered in the Azure and Google Cloud performance tuning articles.
5. Automated and machine clients
Automated clients (schedulers, ETL jobs, point-of-sale devices, integration platforms) tend to reconnect frequently, run many parallel sessions, and occasionally leave connections open.
- Shorten the idle timeout (for example to
300), since automated clients often do not send an explicit disconnect. - Keep the auth idle timeout tight to drop clients that stall before authenticating.
- Mind the IP-banning policy. SFTP Gateway temporarily bans an IP after repeated failed authentications (
features.sftp-subsystem.enable-default-ip-banning-policy=true). A misconfigured automated client that retries with bad credentials can ban itself; fix the credentials rather than disabling the policy. - Raise the database pool. Frequently-reconnecting clients put steady pressure on connection setup, which is database-backed (see section 6). On a busy deployment, raising the pool keeps logins fast.
- Disable SCP if it is unused (
features.sftp-subsystem.scp-enabled) to remove an unused subsystem. - Distribute very large fleets across multiple instances behind a load balancer (section 3).
6. Other high-load settings
Database connection pool
Establishing an SFTP connection is database-backed: login verification, public-key checks, the last-login update, and folder-mount setup each run queries while the connection is set up. The transfer itself is not, so the pool gates how fast connections are established, not throughput. Under a high connection or reconnection rate the default pool of 10 becomes a bottleneck: new logins wait up to connection-timeout for a free database connection and appear slow or stalled, most visibly with frequently reconnecting automated clients and on large instances.
| Property | Default | What it does |
|---|---|---|
spring.datasource.hikari.maximum-pool-size | 10 | Maximum database connections. Raise to 20–30 under load, and to 30–50 on a large instance handling many concurrent connections. |
spring.datasource.hikari.minimum-idle | 5 | Warm connections kept ready for bursts. Raise to 10–20 alongside a larger pool so the warm connections are ready when a burst of logins arrives. |
spring.datasource.hikari.connection-timeout | 30000 (30 s) | How long a request waits for a free database connection before failing. Raising the pool is a better fix for saturation than shortening this, which only makes blocked logins fail faster. |
⚠️ Caution: the database must be sized to accept the larger pool. Setting the pool size very high can overload PostgreSQL and make things worse; make sure the server's
max_connectionsis high enough. In a high-availability deployment, each instance keeps its own pool, so the shared database sees the sum across all instances. Increase in modest steps and watch the database.
Directory listing on cloud backends
features.file-system.ignore-folder-metadata=false by default. Setting it to true skips per-folder metadata lookups during directory listings, which speeds up listing of folders with many entries at the cost of less precise folder timestamps. Leave it at the default unless directory listings are a measured bottleneck.
7. Diagnosing a memory or performance incident
Collect evidence before changing settings. Find the process id, then capture a live heap histogram:
pgrep -f sftpgateway-admin-api
jcmd PID GC.class_histogram | head -40
jmap -histo:live PID | head -40
A histogram dominated by SSH session, transport, or byte-array objects points to connection accumulation or oversized transfer buffers rather than a leak elsewhere. To capture the next incident automatically, add -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/sftpgw/tmp to JAVA_OPTS and restart the service.
Slow logins versus slow transfers. When you investigate slowness, separate the two. Slow or stalled connection establishment points at database pool saturation (section 6). Slow throughput once a transfer is already running points at garbage-collection pauses, network, or the cloud storage backend, since the data path itself does not use the database.
Quick reference
| Goal | Property | Default | Typical high-load value |
|---|---|---|---|
Avoid OutOfMemoryError | -Xmx (JVM option) | auto-sized | Raise with VM/container memory |
| Reclaim idle sessions sooner | sftp.connection-idle-timeout-seconds | 1200 | 300 |
| Drop stalled logins | sftp.auth-idle-timeout-seconds | 120 | 60–120 |
| Real client IP behind a non-AWS load balancer | features.sftp-subsystem.load-balancer.enable-proxy-protocol | false | true (with the ip-addresses list) |
| Large-file part size (S3) | features.file-system.aws-s3.max-simple-upload-size-bytes | 8388608 | 33554432–67108864 |
| Large-file parallelism (S3) | features.file-system.aws-s3.max-inflight-upload-parts | 4 | 2 under high concurrency, higher for raw speed |
| Database pool under load | spring.datasource.hikari.maximum-pool-size | 10 | 20–50, higher on large instances (size the database to match) |
Where to start
Values depend on your file sizes, client behavior, and storage backend. Work through these in order:
- Size
-Xmxfor the instance memory (section 1). This is the effective ceiling on concurrent connections per instance. - Lower
sftp.connection-idle-timeout-secondsso abandoned sessions release memory quickly (section 2). - If one instance cannot hold your peak load comfortably, scale out across several instances behind a load balancer rather than building one ever-larger instance (section 3).
- Raise the database pool (section 6) if connection establishment is slow under load, especially with frequently-reconnecting clients.
- Adjust transfer buffers (section 4) only if large-file throughput is measured as the bottleneck, and raise the heap to cover the added buffer memory.
- Re-measure after each change. Load-test against a copy of production traffic where possible.