Comparison with django-storages¶
django-storages is the de-facto standard and supports many
backends (S3, Azure, Google Cloud, SFTP, Dropbox, …). django-capo-s3 does one thing — S3-compatible
object storage — and does it on the capo-s3 client instead of boto3.
This page compares only the S3 part of the two.
Feature comparison¶
| django-capo-s3 | django-storages[s3] | |
|---|---|---|
| Underlying client | capo-s3 (no boto3) | boto3 / botocore |
| Fully typed, strict-mypy clean | ✅ | ⚠️ partial |
| Media + manifest static storages | ✅ | ✅ |
| Presigned GET / PUT / HEAD URLs | ✅ | ✅ |
| Response overrides in presigned URLs | ✅ | ✅ |
| CloudFront-signed URLs | ✅ | ✅ |
| Response overrides signed into CloudFront URLs | ✅ | ❌ (#1342) |
collectstatic skips unchanged uploads |
✅ built-in | ❌ (needs collectfast, archived) |
| Transparent gzip at rest | ✅ | ✅ |
Text-mode open() of stored files |
✅ | ❌ (#1526) |
| Streaming / concurrent multipart uploads | ✅ | ✅ |
| Bulk delete | ✅ delete_objects() |
➖ per-object |
| Region and bucket routing helpers | ✅ for_region(), for_bucket() |
➖ manual |
| Path-style public URLs for a separate public host | ✅ | ⚠️ (#1142) |
| Refreshable credentials (IRSA / SSO, per request) | ✅ | ⚠️ (#1493) |
| Test doubles for S3 (in-memory service, call assertions) | ✅ django_capo_s3.testing |
❌ (needs moto / boto3 stubs) |
| Backends beyond S3 (Azure, GCS, SFTP, …) | ❌ | ✅ |
| Maturity, adoption, ecosystem | new | very high |
Which should you use?¶
- Use django-storages if you need non-S3 backends, or you want the most battle-tested, widely-deployed option.
- Use django-capo-s3 if you're S3-only and want a fully-typed backend without the boto3 dependency, with the S3-specific rough edges below already handled.
We went through their S3 issues¶
Before building this, we read the open S3 issues in django-storages. Several of them either don't reproduce here (different client, better defaults) or are fixed by design. A few concrete examples:
-
Slow
collectstatic(#1255, #1561) — every asset is re-uploaded on each deploy because the mtime heuristic breaks on fresh checkouts. Hereskip_unchanged(on by default) lists the bucket once and skips unchanged objects by ETag, so an unchanged deploy costs no uploads. -
Text-mode reads of stored files (#1526, #1348) —
open(name, "rt")returnsstr, and gzip-compressed objects are transparently decompressed on read. -
Credentials not refreshed on long-running pods (#1493) — IRSA / web-identity and SSO credentials are resolved per request and refreshed on expiry, instead of being frozen at client construction.
-
CloudFront can't set
Content-Dispositiondynamically (#1342) — response overrides are signed into the CloudFront URL, so they survive to the S3 origin (when the distribution forwards them). -
MinIO behind a separate public host (#1142) —
custom_domain+force_path_styleproduces path-style public URLs (https://public-host/bucket/key) even when the client talks to an internal endpoint.
Better defaults also close a class of confusion outright: listdir uses list_objects_v2, and a missing
bucket raises a clear error instead of a cryptic one.
Migrating? The AWS_* settings you already have keep working — see
Configuration for the full mapping and the one default that differs.
Note. Issue numbers refer to django-storages at the time of writing; some may since have been addressed upstream. The point isn't that django-storages is broken — it's a great, general library — only that a focused S3 backend can sand down these specific edges.