|
| 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) |
0 commit comments