Skip to content

Commit 4abf085

Browse files
committed
Add reproducible S3 storage benchmark
Benchmark S3-compatible providers through one Files SDK workload with network preflights, integrity checks, balanced campaigns, comparison reports, tests, documentation, and CI.
0 parents  commit 4abf085

17 files changed

Lines changed: 2003 additions & 0 deletions

.env.example

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Choose: local, neon, r2, or railway
2+
BENCH_PROVIDER=local
3+
4+
# Shared benchmark settings
5+
BENCH_SIZES=1024,1048576,8388608
6+
BENCH_FILES_PER_SIZE=3
7+
BENCH_CONCURRENCY=2
8+
BENCH_WARMUP_RUNS=1
9+
BENCH_RUNS=3
10+
BENCH_TIMEOUT_MS=120000
11+
BENCH_PREFIX=s3-bench
12+
BENCH_KEEP_OBJECTS=false
13+
BENCH_OUTPUT_DIR=.results
14+
BENCH_NETWORK_PREFLIGHT=true
15+
BENCH_NETWORK_DOWNLOAD_BYTES=10000000
16+
BENCH_NETWORK_UPLOAD_BYTES=2000000
17+
18+
# Local Files SDK filesystem adapter
19+
BENCH_LOCAL_ROOT=.bench-data
20+
21+
# Neon S3-compatible storage
22+
NEON_BUCKET=
23+
AWS_ENDPOINT_URL_S3=https://your-branch.storage.your-region.aws.neon.tech
24+
AWS_ACCESS_KEY_ID=
25+
AWS_SECRET_ACCESS_KEY=
26+
AWS_REGION=us-east-2
27+
28+
# Cloudflare R2 HTTP API
29+
R2_BUCKET=
30+
R2_ACCOUNT_ID=
31+
R2_ACCESS_KEY_ID=
32+
R2_SECRET_ACCESS_KEY=
33+
34+
# Railway Storage Bucket
35+
RAILWAY_S3_ENDPOINT=https://storage.railway.app
36+
RAILWAY_S3_REGION=auto
37+
RAILWAY_S3_BUCKET=
38+
RAILWAY_S3_ACCESS_KEY_ID=
39+
RAILWAY_S3_SECRET_ACCESS_KEY=
40+
RAILWAY_S3_FORCE_PATH_STYLE=false
41+
42+
# Pin AWS SDK v3 to one total attempt so retries cannot hide latency or failures
43+
AWS_MAX_ATTEMPTS=1
44+
45+
# Railway/Bun HTTP service
46+
PORT=3000
47+
BENCH_API_TOKEN=

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: oven-sh/setup-bun@v2
16+
with:
17+
bun-version: 1.3.14
18+
- run: bun install --frozen-lockfile
19+
- run: bun run typecheck
20+
- run: bun test

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
node_modules/
2+
.env
3+
.env.*
4+
!.env.example
5+
.results/
6+
.bench-data/
7+
coverage/
8+
.DS_Store

CONTRIBUTING.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Contributing
2+
3+
Thanks for helping improve S3 Bench.
4+
5+
## Development
6+
7+
1. Install [Bun](https://bun.sh/) 1.2 or newer.
8+
2. Run `bun install`.
9+
3. Copy `.env.example` to `.env` only if you need a remote provider.
10+
4. Make the smallest change that solves the problem.
11+
5. Run `bun run typecheck` and `bun test` before opening a pull request.
12+
13+
Use the local filesystem adapter for routine development and tests:
14+
15+
```bash
16+
bun run benchmark:local
17+
```
18+
19+
## Benchmark changes
20+
21+
Changes to timing, concurrency, retries, payload generation, integrity checks, aggregation, or provider ordering can invalidate comparisons. Include a test or a reproducible report showing the behavior, and explain any methodology change in the pull request.
22+
23+
Never commit credentials, signed URLs, bucket identifiers, raw `.env` files, or generated `.results/` reports. Remote benchmarks may incur storage, request, and egress charges, so use isolated test buckets and keep cleanup enabled.
24+
25+
## Pull requests
26+
27+
Keep pull requests focused. Describe what changed, why it is fair across providers, and which validation commands you ran.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 S3 Bench contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# S3 Bench
2+
3+
A reproducible object-storage benchmark for S3-compatible providers, built on [Files SDK](https://files-sdk.dev/). It runs the same upload, HEAD, download, LIST, integrity, and cleanup workload against local storage, Neon, Cloudflare R2, and Railway Storage Buckets.
4+
5+
S3 Bench records raw samples and produces both JSON and readable Markdown reports. It is designed for honest provider comparisons, so retries are disabled, downloads are SHA-256 verified, network conditions are recorded, and public campaigns rotate provider order.
6+
7+
## Quick start
8+
9+
Requires [Bun](https://bun.sh/) 1.2 or newer.
10+
11+
```bash
12+
bun install
13+
cp .env.example .env
14+
chmod 600 .env
15+
bun run benchmark:local
16+
```
17+
18+
The local benchmark needs no cloud credentials. Reports are written to `.results/`, and temporary objects are removed after each run.
19+
20+
## What it measures
21+
22+
The default workload uses 1 KiB, 1 MiB, and 8 MiB objects, with three objects per size, concurrency two, one warmup cycle, and three measured cycles.
23+
24+
Each measured cycle:
25+
26+
1. Uploads every object.
27+
2. Runs HEAD and verifies the stored byte size.
28+
3. Downloads every object and verifies its length and SHA-256 hash.
29+
4. Lists the run prefix and verifies every expected key exists.
30+
5. Deletes every uploaded object in a `finally` block.
31+
32+
Within upload, HEAD, and download, each size runs as a separate concurrent phase. Measured cycles rotate the size order so one size does not permanently benefit from running first or last.
33+
34+
Before each provider run, S3 Bench also records a Cloudflare HTTP preflight consisting of a small request, a 10 MB download, and a 2 MB upload. This is a connection snapshot, not a direct latency measurement to the storage provider.
35+
36+
## Configure a provider
37+
38+
Copy `.env.example` to `.env`, then fill in one provider section. Keep `.env` local and mode `600`.
39+
40+
### Neon
41+
42+
```dotenv
43+
NEON_BUCKET=your-bucket
44+
AWS_ENDPOINT_URL_S3=https://your-branch.storage.your-region.aws.neon.tech
45+
AWS_ACCESS_KEY_ID=generated-key-id
46+
AWS_SECRET_ACCESS_KEY=generated-secret
47+
AWS_REGION=us-east-2
48+
```
49+
50+
The Neon adapter uses path-style addressing through Files SDK's AWS SDK v3 S3 client.
51+
52+
### Cloudflare R2
53+
54+
```dotenv
55+
R2_BUCKET=your-bucket
56+
R2_ACCOUNT_ID=your-account-id
57+
R2_ACCESS_KEY_ID=generated-key-id
58+
R2_SECRET_ACCESS_KEY=generated-secret
59+
```
60+
61+
The benchmark explicitly selects Files SDK's AWS SDK client path for R2 instead of its optional fetch client.
62+
63+
### Railway Storage Bucket
64+
65+
```dotenv
66+
RAILWAY_S3_ENDPOINT=https://storage.railway.app
67+
RAILWAY_S3_REGION=auto
68+
RAILWAY_S3_BUCKET=your-bucket
69+
RAILWAY_S3_ACCESS_KEY_ID=generated-key-id
70+
RAILWAY_S3_SECRET_ACCESS_KEY=generated-secret
71+
RAILWAY_S3_FORCE_PATH_STYLE=false
72+
```
73+
74+
Use the values and URL style shown in Railway's Bucket credentials tab. Older buckets can require path-style URLs. Railway documents that its Storage Buckets run on Tigris infrastructure, but S3 Bench treats Railway Bucket as the product being measured.
75+
76+
All remote providers use the same Files SDK AWS SDK v3 S3Client path. Files SDK retries are disabled, and `AWS_MAX_ATTEMPTS=1` limits the underlying AWS SDK to one total attempt so retries cannot hide failures or inflate latency.
77+
78+
## Run benchmarks
79+
80+
Run one provider:
81+
82+
```bash
83+
bun run benchmark -- --provider r2
84+
bun run benchmark -- --provider neon
85+
bun run benchmark -- --provider railway
86+
```
87+
88+
Override the workload when needed:
89+
90+
```bash
91+
bun run benchmark -- \
92+
--provider r2 \
93+
--sizes 16777216,67108864,104857600 \
94+
--files 2 \
95+
--concurrency 2 \
96+
--warmups 1 \
97+
--runs 3
98+
```
99+
100+
Sizes are bytes. Use `--skip-network` only for tests or offline development. `--keep` leaves uploaded objects behind; cleanup is enabled by default.
101+
102+
### Balanced public campaign
103+
104+
A single sequential pass is vulnerable to provider-order and transient network effects. The campaign command runs three Latin-square rounds, so every provider appears once in each sequence position.
105+
106+
```bash
107+
bun run campaign
108+
bun run publication
109+
```
110+
111+
The publication generator validates that all nine reports used an identical workload, one AWS SDK attempt, balanced positions, and zero failures before producing aggregate and per-size tables.
112+
113+
`bun run compare` remains available for a quick comparison of the latest individual reports, but the balanced campaign is the appropriate path for published results.
114+
115+
### Large objects
116+
117+
The shared `files.upload()` path is a single object upload. Provider limits can differ. Neon accepted 100 MiB in testing but rejected 128 MiB and 256 MiB single uploads; Neon recommends multipart upload through the AWS SDK for larger files. Multipart is outside this benchmark's shared Files SDK operation and should be tested as a separate workload rather than mixed into these results.
118+
119+
## Reports
120+
121+
Each run writes:
122+
123+
- JSON containing every raw operation sample, workload configuration, runtime context, network preflight, integrity failures, cleanup errors, and run errors.
124+
- Markdown containing p50 and p95 request latency plus aggregate wall-clock throughput.
125+
126+
Generated reports live in `.results/` and are intentionally ignored by Git because they may contain machine, bucket, endpoint, and timing metadata. Review any report before publishing it.
127+
128+
## HTTP service
129+
130+
S3 Bench can run as a small authenticated Bun service, including on Railway. Set provider credentials and a strong `BENCH_API_TOKEN`, then start it with:
131+
132+
```bash
133+
bun run start
134+
```
135+
136+
```bash
137+
curl -X POST https://YOUR-SERVICE.example/benchmark \
138+
-H "Authorization: Bearer $BENCH_API_TOKEN" \
139+
-H "Content-Type: application/json" \
140+
-d '{"provider":"neon"}'
141+
```
142+
143+
Health check:
144+
145+
```bash
146+
curl https://YOUR-SERVICE.example/health
147+
```
148+
149+
Only one HTTP benchmark runs at a time. Attach persistent storage or retain the returned JSON if reports must survive redeploys.
150+
151+
## Interpreting results
152+
153+
Benchmark numbers are observations, not universal provider rankings. Region placement, Wi-Fi conditions, internet routing, object size, concurrency, hot versus cold reads, and provider limits all affect the outcome. Compare per-size tables, inspect round consistency, and publish the recorded limitations with the numbers.
154+
155+
Running this project against paid storage can incur request, storage, and egress charges. Start with the local adapter, use isolated test buckets, and keep cleanup enabled.
156+
157+
## Development
158+
159+
```bash
160+
bun run typecheck
161+
bun test
162+
```
163+
164+
## Contributing
165+
166+
See [CONTRIBUTING.md](CONTRIBUTING.md). Security issues should follow [SECURITY.md](SECURITY.md).
167+
168+
## License
169+
170+
[MIT](LICENSE)

SECURITY.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Security Policy
2+
3+
## Reporting a vulnerability
4+
5+
Please do not open a public issue for vulnerabilities involving credential exposure, authentication bypass, arbitrary object access, or destructive storage behavior.
6+
7+
Report the issue privately through GitHub's security advisory feature for this repository. Include reproduction steps, affected versions or commits, and the potential impact. Do not include live cloud credentials or signed URLs.
8+
9+
## Supported versions
10+
11+
Until the project publishes versioned releases, security fixes apply to the latest commit on the default branch.
12+
13+
## Credential safety
14+
15+
S3 Bench reads cloud credentials from environment variables. `.env`, generated reports, local benchmark data, and dependency directories are ignored by Git, but contributors should still review changes and reports before sharing them publicly.

0 commit comments

Comments
 (0)