Performance

Choose AsyncUploader(chunk_size=…), S3 multipart part_size, and uvicorn workers by typical file size and concurrency. Guidance below comes from UploadKit’s async/sync pipelines and k6 runs in the uploadkit-testing perf/ harness (MinIO + FastAPI, local Docker).

What actually exists

Upload tuning knobs
KnobApplies toDefault in Core / harnessEffect
AsyncUploader.chunk_sizeAsync onlyCore 1 MiB; harness often 8 MiBBytes read per loop → validators → storage writer
S3 multipart part_sizeAsync S3/MinIO writer5 MiB (S3 minimum except last part)How large each upload_part is
Sync “chunk”NoneSync does one full file.read() then put_object
Uvicorn workersProcess concurrencyHarness A/B used 4Parallel uploads across processes

Sync has no chunk size. For large or concurrent files, prefer async. See Core and Storage.

Measured results

Environment: Docker Desktop, MinIO + API on one machine, k6 on host. Treat small % deltas as noise.

Async read chunk_size — 1 MiB vs 8 MiB

Checksum on, 1 worker, async+sync mixed:

chunk_size A/B latency
Payload1 MiB avg8 MiB avgDelta
10 MB PNG (40 reqs)7.42 s7.44 s~0%
100 MB PDF (10 reqs)35.74 s35.28 s~−1%

Conclusion: Changing async read chunk between 1–8 MiB barely moves end-to-end latency here. Pick for memory / simplicity, not raw speed.

S3 part_size — 5 MiB vs 16 MiB

Checksum off, 4 workers, async only:

part_size A/B latency
Payload5 MiB avg16 MiB avgDelta
10 MB PNG (20 reqs)3.90 s3.85 s~−1.5%
100 MB PDF (5 reqs)17.41 s18.10 s~+4%

Conclusion: 16 MiB parts did not clearly beat 5 MiB. Prefer 5 MiB unless you re-benchmark on real AWS with higher concurrency.

What did move the needle

High-impact tuning levers
ChangeEffect on wall time
Async vs sync for large filesSync holds the whole object in RAM; avoid under load
Drop SHA-256 checksumLarge CPU save on big bodies
1 → 4 uvicorn workersBetter parallel throughput under concurrent VUs
chunk_size / part_size tweaksSmall / noise-level in these runs

Recommendation matrix

Let F = typical upload size, W = uvicorn (or Gunicorn) workers, C = expected concurrent uploads per worker (roughly k6 VUs / W under load).

Peak RAM ballpark:

  • Async:W × C × max(chunk_size, part_size) (+ small overhead)
  • Sync:W × C × F (full file buffered)

By average file size

Suggested sizes by average upload
Average file size FPathSuggested chunk_sizeSuggested S3 part_sizeNotes
< 1 MBSync OKn/an/a (single put)Keep it simple
1–10 MBPrefer async1 MiB5 MiBPart size ≥ file → often one part after buffer flush
10–50 MBAsync1–8 MiB5–8 MiB8 MiB chunk is fine; no proven latency win over 1 MiB
50–200 MBAsync only8 MiB5–8 MiBAvoid sync; consider disabling checksum if product allows
> 200 MBAsync only8 MiB8–16 MiBRe-benchmark part_size on target cloud

By worker count

Suggested sizes by worker count
Workers WConcurrent uploads (total)Guidance
1Lowchunk_size=1 MiB, part_size=5 MiB is enough
2–4Medium (e.g. 4–16 VUs)Keep chunk_size8 MiB, part_size 5 MiB; watch RAM ≈ W×C×8 MiB
8+HighPrefer smaller chunks (1 MiB) so W×C×chunk stays bounded; scale horizontally before enlarging buffers

Rule of thumb: W × C × chunk_size ≲ 10–20% of container memory. Example: 4 workers × 4 concurrent × 8 MiB ≈ 128 MiB buffers alone — fine on a 1–2 GiB service; raise carefully.

Decision cheat sheet

decision.txt
if F < 1MB:
    use sync Uploader (or async; either fine)
elif need max throughput under concurrency:
    async + checksum off (if allowed) + W=2..4 + chunk_size=1MiB + part_size=5MiB
elif need integrity (sha256) + large F:
    async + checksum on + chunk_size=1..8MiB + part_size=5MiB + W sized for RAM
else:
    async + chunk_size=1MiB + part_size=5MiB   # safe default

Do not expect “5–8 MiB chunks are always faster than 1 MiB” — local A/B did not support that for e2e latency.

Reproduce

Harness env knobs: ASYNC_CHUNK_SIZE, S3_PART_SIZE, ENABLE_CHECKSUM, UVICORN_WORKERS (reported on GET /health). Part-size A/B:

Shell
cd uploadkit-testing/perf
./scripts/ab_part_size.sh

Limitations

  • Local MinIO on Docker Desktop — not AWS cross-AZ latency.
  • Small iteration counts for PDF A/B (5 reqs); treat ± a few % as noise.
  • Async+sync mixed runs inflate averages vs async-only.
  • No parallel multipart uploads yet (parts are sequential) — future work may beat size tuning.

When in doubt: async, chunk_size=1 MiB, part_size=5 MiB, size workers to concurrency and RAM, then re-run load tests on your real target.

Related: Core · Storage · FastAPI · Full markdown guide: CHUNK_SIZE_GUIDE.md