I built s3bench to answer a narrow question: how does an S3 endpoint behave when one large object is fetched through concurrent HTTP range requests? It controls the chunk size and concurrency, then records the throughput and latency of that access pattern.
That result is not a general score for AWS S3, MinIO, Ceph or any other backend. The client, network, object shape, endpoint, cache state and output mode are all part of the measurement.
Why Parallel Range Requests Matter
S3 defines a Multipart Upload API, but there is no equivalent “multipart download” API. For downloads, s3bench sends concurrent GetObject requests with HTTP Range headers. Instead of reading a 10GiB object as a single stream, it can:
- Split the object into chunks using HTTP
Rangeheaders - Download multiple chunks concurrently
- Write the chunks in order when
--outputis used, or discard them when measuring the transfer path alone
This approach can improve throughput for large objects and high-latency paths. It is not guaranteed to do so: the optimum depends on the endpoint, link, client CPU and service limits.
How s3bench Works
The tool does five things:
- HeadObject - Determines the object size
- Chunk Division - Splits the object into configurable byte-range chunks
- Concurrent Download - Dispatches goroutines to fetch chunks in parallel using
Rangeheaders - Live Progress - Shows real-time transfer rates (updated every 200ms)
- Detailed Reporting - Reports throughput, time-to-first-byte, and latency percentiles
Get s3bench
GitHub repository: soothill/s3bench
You can:
- Clone the repository:
git clone https://github.com/soothill/s3bench.git -
Download releases: Visit the release page for pre-built binaries
- Browse the source code: The behaviour described here was checked against commit
49344ec
Installing Go
s3bench requires Go 1.22 or later. Here’s how to install Go on different platforms:
Linux
Ubuntu or Debian:
sudo apt update
sudo apt install golang-go
go version
Fedora, RHEL or CentOS Stream:
sudo dnf install golang
go version
Arch Linux:
sudo pacman -S go
go version
If the distribution package is older than Go 1.22, use the signed archive and instructions from the official Go downloads page.
macOS
With Homebrew:
brew install go
go version
The official macOS package is also available from go.dev/dl.
Windows
Use the official Windows installer from go.dev/dl. After installation, open a new PowerShell window and verify the toolchain:
go version
Building from Source
Once Go is installed:
cd s3bench
go mod tidy
go build -o s3bench .
Basic Usage
./s3bench \
--bucket my-bucket \
--key path/to/large-file.bin \
--chunk-size 64MB \
--concurrency 16 \
--discard
Use --discard when the question is specifically about the network and object-store path; it prevents local disk I/O from becoming the bottleneck. Use --output when the end-to-end download path, including the destination filesystem, is what matters.
Key Features
Chunk Size Presets
Instead of remembering byte values, use intuitive presets:
| Preset | Size |
|---|---|
| XS | 1 MiB |
| S | 4 MiB |
| M | 8 MiB |
| L | 64 MiB |
| XL | 256 MiB |
| XXL | 1 GiB |
./s3bench --chunk-size L --bucket mybucket --key bigfile.bin --discard
Concurrency Sweep
Use a concurrency sweep to find where this particular path stops gaining throughput. Pass several worker counts in one run:
./s3bench \
--bucket my-bucket \
--key path/to/large-file.bin \
--chunk-size 64MB \
--concurrency 4,8,16,32,64 \
--runs 3 \
--discard
This produces a comparison report:
╔══════════════════════════════════════════════════════════╗
║ Concurrency Sweep Comparison ║
╚══════════════════════════════════════════════════════════╝
Workers Runs Min MB/s Mean MB/s Max MB/s
------- ---- -------- --------- --------
4 3 412.1 438.7 461.3
8 3 781.4 823.9 856.2
16 3 1102.5 1163.8 1201.4
32 3 1367.9 1401.5 1423.0 <-- best
64 3 1389.2 1398.1 1412.7
The tool currently labels binary units as MB and GB; its calculations divide by powers of two, so those values are MiB/s and GiB/s in IEC terminology.
S3-Compatible Storage
Works with MinIO, Ceph, and any S3-compatible endpoint:
./s3bench \
--endpoint http://minio.local:9000 \
--bucket testbucket \
--key bigfile.bin \
--access-key-id minioadmin \
--secret-access-key minioadmin \
--region us-east-1 \
--chunk-size 64MB \
--concurrency 8,16,32 \
--runs 3 \
--discard
Detailed Output
Per-Run Summary
=== Run 1 ===
Object: s3://my-bucket/path/to/large-file.bin
Object size: 10.00 GB
Chunk size: 64.00 MB (160 chunks)
Concurrency: 16 workers
Results:
Total time: 8.432 s
Total bytes: 10.00 GB
Throughput: 1184.3 MB/s (1.157 GB/s)
Time to 1st byte: 42.3 ms
Chunk latency (per-chunk download time):
Min: 341.2 ms
Max: 892.7 ms
Mean: 526.4 ms
P50: 512.1 ms
P95: 781.3 ms
P99: 856.4 ms
JSON Output
For scripting and automation:
./s3bench --bucket b --key k --concurrency 8,16,32 --discard --json \
| jq '.[] | {workers: .Concurrency, mean_mb_s: .Aggregate.mean_throughput_mb_s}'
Known interface caveat: in commit 49344ec, duration fields such as total_time_ms and ttfb_ms are serialised from Go’s time.Duration without conversion. Their numeric values are nanoseconds despite the _ms suffix. Convert them before use—for example, divide by 1,000,000 for milliseconds—and pin the tool revision in automated reports until the schema is corrected.
How I would run the comparison
Use these as starting points, then retain the settings and conditions with the result:
1. Sweep concurrency
Run --concurrency 4,8,16,32,64 to locate the plateau. A plateau shows that some part of the measured path has saturated; it does not identify the bottleneck by itself.
2. Balance Chunk Size and Concurrency
- Smaller chunks + more workers = more connection overhead
- Larger chunks + fewer workers = potential worker idle time
- Starting point: try
--chunk-size L --concurrency 16, then sweep in both directions
3. Use Multiple Runs and Record the Conditions
Use --runs 3 or more and report the spread, not only the best result. A slower first run might reflect DNS, connection setup, TLS, a client-side cache or an object-store cache; do not attribute it to storage without evidence.
4. Match the Output Mode to the Question
Use --discard for network and object-store throughput. Use --output when validating the complete path to local storage, and verify the resulting object separately if integrity is part of the test.
5. Test from the Right Location
Results are only meaningful when measured from where your workload actually runs. For example:
- EC2 instance in the same region as the bucket (for AWS)
- Same network segment as your MinIO deployment
- Avoid a laptop over VPN unless that path is the workload you intend to measure
All Command-Line Options
| Flag | Default | Description |
|---|---|---|
--bucket |
(required) | S3 bucket name |
--key |
(required) | S3 object key to download |
--chunk-size |
64MB |
Size of each byte-range read |
--concurrency |
8 |
Parallel workers (single or comma-separated) |
--runs |
1 |
Repetitions per concurrency level |
--profile |
impossible |
AWS named profile |
--access-key-id |
"" |
Override access key |
--secret-access-key |
"" |
Override secret key |
--region |
us-east-1 |
AWS region |
--endpoint |
"" |
Custom S3-compatible endpoint |
--discard |
false |
Discard downloaded bytes |
--output |
"" |
Write to file instead |
--json |
false |
JSON output |
What the result can and cannot say
Parallel ranged GETs are one useful way to exercise high-throughput object access. They can show where throughput plateaus for a recorded object, client, endpoint, chunk size and worker count. They cannot, on their own, identify whether the limiting component is the client, network or storage backend, and they do not represent small-object or whole-object streaming workloads.
Use the same object shape, output mode and client location when comparing systems or releases. Report the spread across repeated runs rather than only the best result, and keep the tool revision with the data. The source, current documentation and release status are in the s3bench GitHub repository; it is published under the MIT licence.