Reusable VM benchmark harness for comparing S3-compatible providers from a Linux test node.
The scripts write JSONL and logs to /dataoutput, use generated payloads in /testfiles, and use /downloads as disposable download scratch space.
Run this on a fresh apt-based Linux VM:
git clone https://github.com/fil-one/s3-speedtests
cd s3-speedtests
sudo ./scripts/setup_vm.shThe setup installs traceroute, ping, jq, curl, Python, python3-docx, python3-reportlab, Ookla speedtest, and AWS CLI. It also generates:
100 x 1 MiBfiles5 x 100 MiBfiles1 x 1 GiBfile1 x 25 GiBfile1 x 50 GiBfile
To install dependencies without generating the full payload set:
sudo ./scripts/setup_vm.sh --skip-filesTo create or repair only the /testfiles payload set after dependencies are already installed:
sudo ./scripts/setup_vm.sh --files-onlyTo regenerate files even when matching filenames already exist:
sudo ./scripts/setup_vm.sh --files-only --force-filesTo run the scripts from a laptop (macOS or Linux) instead of the VM, generate
the payload set without root into any directory and point the scripts at it
with --testfiles-dir:
./scripts/generate_test_files.sh ~/s3-testfiles quick # 1 MiB + 100 MiB
./scripts/generate_test_files.sh ~/s3-testfiles standard # everything up to 1 GiB
./scripts/generate_test_files.sh ~/s3-testfiles large # 1 GiB + 25 GiB + 50 GiB
FORCE_FILES=1 ./scripts/generate_test_files.sh ~/s3-testfiles quick # regenerate even if sizes matchSetup seeds /testfiles/s3_targets.ini from config/s3_targets.example.ini if it does not already exist.
Edit /testfiles/s3_targets.ini, enable the providers you want, and add bucket names. For static-key providers, add access keys there and then write AWS CLI profiles:
./scripts/s3_write_profiles.pyFor AWS SSO or temporary AWS credentials, configure the profile directly with AWS CLI and set profile = aws and auth_mode = profile.
S3 commands default to AWS_MAX_ATTEMPTS=11, which means one initial request plus ten retries. Override globally with AWS_MAX_ATTEMPTS=... or per provider with aws_max_attempts = ... in /testfiles/s3_targets.ini.
Warning: run_all executes the full benchmark workflow after setup. It writes AWS CLI profiles from /testfiles/s3_targets.ini, checks access, uploads the standard and large file sets, downloads all uploaded objects, runs network tests and traceroutes, and then builds the default DOCX report. This can move more than 75 GiB per enabled provider in each direction and may incur cloud egress, storage, API, or bandwidth costs.
Run the complete workflow:
./scripts/run_allRun only selected providers:
./scripts/run_all --providers aws,wasabiShow AWS CLI transfer progress in the tmux console during upload and download steps:
./scripts/run_all --progressThe same behavior can be enabled with TRANSFER_PROGRESS=1:
TRANSFER_PROGRESS=1 ./scripts/run_all --providers aws,wasabiUpload and download up to four files concurrently per provider:
./scripts/run_all --parallelChoose a different concurrency limit with --parallel-workers N. The same settings are available through PARALLEL_TRANSFERS=1 and PARALLEL_WORKERS=N.
By default, run_all uses minimal waits: PAUSE_SECONDS=5, NETWORK_RUNS=1, NETWORK_SLEEP_SECONDS=5, and NETWORK_SERVER_MODE=world. Override them when you want more samples:
NETWORK_RUNS=3 NETWORK_SLEEP_SECONDS=30 PAUSE_SECONDS=10 ./scripts/run_allThe orchestration log is written to /dataoutput/run_all_<timestamp>.log.
The final report step prompts for the source node provider/name and location unless those values are supplied through environment variables:
SOURCE_NODE_PROVIDER="AWS EC2" \
SOURCE_NODE_LOCATION="eu-west-3 | Paris, France" \
SOURCE_NODE_NETWORK="1 Gbit connection" \
./scripts/run_all --providers aws,wasabiCheck bucket access:
./scripts/s3_access_check.pyCheck one provider by target section, provider name, or AWS profile:
./scripts/s3_access_check.py --providers f1
./scripts/s3_access_check.py --providers aws-us-west-2Print each AWS CLI command plus stdout/stderr for debugging:
./scripts/s3_access_check.py --providers f1 --verbosePass AWS CLI --debug for deep troubleshooting. This is very noisy and can include request metadata, so redirect it to a log when needed:
./scripts/s3_access_check.py --providers f1 --debug-aws 2>&1 | tee /dataoutput/f1_access_debug.logNetwork baseline:
RUNS=1 ./scripts/network_speedtest_ookla.shBy default, the network baseline uses the configured world server list so results cover the same explicit cities on every VM. Use NETWORK_SERVER_MODE=auto when only Ookla's nearby/best server is wanted.
Network server modes:
NETWORK_SERVER_MODE=auto RUNS=1 ./scripts/network_speedtest_ookla.sh
NETWORK_SERVER_MODE=world RUNS=1 ./scripts/network_speedtest_ookla.sh
NETWORK_SERVER_MODE=fixed RUNS=1 ./scripts/network_speedtest_ookla.sh
NETWORK_SERVER_MODE=geo RUNS=1 ./scripts/network_speedtest_ookla.sh
NETWORK_SERVER_MODE=all RUNS=1 ./scripts/network_speedtest_ookla.shauto uses Ookla's selected server and does not call the Speedtest server-list API. world uses the configured global server list without also running automatic-nearest, preventing the nearest location from duplicating one of the explicit cities. fixed uses the legacy Barcelona, Madrid, and Paris server IDs. geo searches the Speedtest server-list API for the configured city coordinates and may be rate-limited by Ookla. all combines auto, fixed, and geo. Reports also suppress an automatic-nearest row when historical data contains an explicit target for the same resolved city and country.
Provider traceroutes:
./scripts/s3_provider_traceroutes.shTraceroutes are generated only for enabled endpoints in /testfiles/s3_targets.ini. An explicit port in endpoint_url is used as the TCP traceroute destination port; otherwise HTTPS endpoints default to 443 (and explicit HTTP URLs default to 80). Use --targets to point at a different target config.
Upload standard and large file sets together:
./scripts/s3_upload_speedtest.sh --file-set fullShow upload progress and per-object elapsed time / throughput in the console:
./scripts/s3_upload_speedtest.sh --file-set full --progressUpload files concurrently (four workers by default):
./scripts/s3_upload_speedtest.sh --file-set full --parallel
./scripts/s3_upload_speedtest.sh --file-set full --parallel --parallel-workers 8Run upload file sets separately when you want independent standard and large result files:
./scripts/s3_upload_speedtest.sh --file-set standard
./scripts/s3_upload_speedtest.sh --file-set largeDownload all uploaded files:
./scripts/s3_download_speedtest.sh --file-set fullShow download progress and per-object elapsed time / throughput in the console:
./scripts/s3_download_speedtest.sh --file-set full --progressDownload files concurrently (four workers by default):
./scripts/s3_download_speedtest.sh --file-set full --parallel
./scripts/s3_download_speedtest.sh --file-set full --parallel --parallel-workers 8Parallel mode overlaps files within one provider and repeat. Providers and repeat rounds still run in sequence, and cleanup waits for all downloads in the batch to finish. With --progress, output from simultaneous AWS CLI processes can be interleaved.
With --progress, the AWS CLI progress display streams to the terminal. The scripts still write JSONL metrics and print a final DONE ... elapsed=... throughput_mbps=... line for each object. When progress is enabled through run_all, the orchestration log also captures the console stream because run_all uses tee.
Watch newest logs:
tail -f "$(ls -t /dataoutput/*.log | head -n 1)"Preview deletion of test objects from all enabled providers. This is a dry run by default:
./scripts/s3_cleanup_test_files.pyActually delete each provider's configured test prefix from all enabled providers:
./scripts/s3_cleanup_test_files.py --executeClean only selected providers:
./scripts/s3_cleanup_test_files.py --providers aws,wasabi --executeDelete a specific prefix instead of each provider's configured prefix:
./scripts/s3_cleanup_test_files.py --providers f1 --prefix test/20260715T185306Z --executeDelete all objects in selected buckets:
./scripts/s3_cleanup_test_files.py --providers f1 --entire-bucket --executeCleanup audit records are written to /dataoutput/s3_cleanup_test_files.jsonl.
Build a summary report from JSONL output in /dataoutput:
./scripts/build_summary_report.pyPDF is the default format. Generated reports are written under /dataoutput/reports.
Choose a report format explicitly:
./scripts/build_summary_report.py --format docx
./scripts/build_summary_report.py --format pdf
./scripts/build_summary_report.py --format bothThe report builder loads the latest available benchmark artifacts:
/dataoutput/network_speedtest_ookla_summary.jsonl/dataoutput/s3_upload_speedtest_summary.jsonlor the latests3_upload_speedtest_summary_*.jsonl/dataoutput/s3_download_speedtest_summary.jsonlor the latests3_download_speedtest_summary_*.jsonl/dataoutput/s3_provider_traceroutes.jsonlor the latests3_provider_traceroutes_*.jsonl
Upload and download ranking cells include median throughput, average throughput, total elapsed time, and median elapsed time when available. In each total-time chart, a provider with any failed transfer is marked FAILED and ranked after every provider whose transfers all succeeded, so incomplete elapsed time cannot improve its ranking. The traceroute section includes only enabled target endpoints from the target config, plus the full CLI traceroute command output with hop lines.
Provider names and regions in the upload/download result tables are read from /testfiles/s3_targets.ini, so changing a bucket target region there changes the report label on the next report build. Optional display_name and location fields in each target section override the report label, for example display_name = AWS and location = Paris, France. Use --targets to point at a different target config.
When run interactively, the report builder prompts for the source node provider/name and source node location. It auto-detects hostname, vCPU count, and RAM from the VM.
The Test Node section includes the UTC time the benchmark suite started, formatted as 14:07:32 (UTC) on September 20, 2026. scripts/run_all passes this time to the report builder automatically. When rebuilding a report directly, the builder infers an approximate start time from timestamped artifacts in /dataoutput, the first saved network-test record, or a data-file modification time. You can set it explicitly with --tests-started-at-utc 20260920T140732Z.
Prompted run:
./scripts/build_summary_report.pyExpected prompts:
Source node provider/name:
Source node location:
For non-interactive runs:
./scripts/build_summary_report.py \
--format docx \
--source-provider "AWS EC2" \
--source-location "eu-west-3 | Paris, France" \
--node-network "1 Gbit connection" \
--no-promptYou can also use environment variables:
SOURCE_NODE_PROVIDER="AWS EC2" \
SOURCE_NODE_LOCATION="eu-west-3 | Paris, France" \
SOURCE_NODE_NETWORK="1 Gbit connection" \
./scripts/build_summary_report.py --format pdf --no-promptUse explicit input and output directories when rebuilding a report from archived results:
./scripts/build_summary_report.py \
--format both \
--targets /testfiles/s3_targets.ini \
--data-dir /dataoutput \
--output-dir /dataoutput/reports