Cut the Egress Cost of ZIP Exports
The bandwidth math of a "download all" feature — why proxying files multiplies egress, what each storage provider charges, and the prepare-once pattern that caps the cost at one pass.
Bandwidth is the invisible line item of every "download all as ZIP" feature. The files are already paid for in storage; what surprises teams is that serving them can cost more than storing them — especially when the architecture quietly pays egress more than once per file.
Where the money leaks
Count how many times a byte leaves a paid network boundary:
- Proxy-per-download (the classic server-side ZIP): bucket → your server → user, on every download. A 20 GB export from S3 downloaded three times moves 60 GB out of AWS: ~$5.40, forever coupled to how often users click.
- Browser-side ZIP with signed URLs: bucket → user directly. One pass per download — better, but still per-download, and re-fetching every object on a retry.
- Prepare-once (stored job): bucket → archive, once. Repeat downloads serve the prepared archive; the bucket is never read again for that export.
The pattern, not the provider, is usually the biggest lever: going from proxy-per-download to prepare-once divides source egress by the number of downloads.
What providers actually charge
Standard published rates at the time of writing (verify against current pricing pages — regions and tiers vary):
| Source | Egress rate | Notes |
|---|---|---|
| AWS S3 | ~$0.09/GB | After 100 GB/month free across the account (US regions, first 10 TB tier) |
| Cloudflare R2 | $0 | Read operations bill per-request (cents/million); bytes are free |
| Supabase Storage | $0.09/GB over quota | Unified project quota: 5 GB free plan, 250 GB included on Pro |
| Google Cloud Storage | ~$0.12/GB | Region-dependent |
| Backblaze B2 | $0.01/GB | Free up to 3× stored data; free via Cloudflare's network |
Two readings of that table:
- If your exports live on S3 or GCS, the pattern matters most — you can't change the rate, so pay it once per export instead of once per download.
- If you control where new data lands, R2 (or B2) changes the game — a ZIP pipeline from R2 through Eazip has no per-GB bandwidth charge on either leg (Eazip adds no outbound-bandwidth fee: zero-egress exports).
The prepare-once pattern
One job per export; the default stored mode fetches each source a single time while preparing the archive, and every subsequent download serves the prepared copy:
await fetch('https://api.eazip.io/jobs', {
method: 'POST',
headers: {
'X-API-Key': process.env.EAZIP_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
files, // presigned URLs — see the platform guides
zip_filename: 'export.zip',
expires_in: 7 * 86400,
}),
});Cost shape: (source egress × archive size × 1) + Eazip plan quota (stored GB-days). The per-platform signing code and each platform's numbers: S3, R2, Supabase Storage.
Note the honest counterpoint: stream mode inverts this. Streamed jobs re-fetch sources per download — the right choice for downloaded-once archives (no retention quota), the wrong one for a gallery grabbed by the whole family. Stream or stored is the decision page.
Three checks before you optimize anything
- Are you proxying? If export bytes pass through your app servers, fixing that beats every other change — presign and let the fetcher go to the bucket directly.
- Are repeat downloads re-reading the bucket? Logs will show the same objects fetched for the same export. That is the prepare-once gap.
- Is a CDN actually helping? Signed URLs with unique query strings defeat caching, and most one-off exports are never fetched twice from the same edge anyway — CDNs solve popular-content bandwidth, not export bandwidth.
FAQ
Does moving the ZIP work to a service eliminate S3 egress?
No — someone still reads each object from S3 once, and AWS bills that pass. What the prepare-once pattern eliminates is every pass after the first. Zero source egress exists only where the source itself charges zero (R2, or B2 within its free allowances).
Is browser-side zipping cheaper than a managed job?
For a single download of a small export, they're equivalent (one pass either way) and the browser path has no service involved. The managed job wins on repeat downloads — the browser re-fetches everything, the stored archive doesn't.
We're on S3 — should we migrate to R2 for this?
Not for exports alone unless they dominate your bandwidth bill. But new write paths that exist mainly to be re-downloaded (deliverables, galleries, report bundles) are natural R2 candidates: same S3 API, $0.09/GB → $0.
Fix AWS Lambda Timing Out While Zipping Files
Why Lambda ZIP jobs die at 15 minutes (or 29 seconds), the quick fixes — streaming, memory, direct invocation — and the structural fix of moving the fetch-and-zip work out of the function.
Zip Thousands of Files Without Melting Anything
What actually breaks when a ZIP job grows from 50 URLs to 20,000 — list transport, signing at scale, partial failures, and output splitting — and the server-side pattern that handles each.