EAZIP BLOG

4 Ways to Download an Entire S3 Bucket as a ZIP

S3 has no native "download folder as ZIP" button. Here are four ways teams actually build one, compared honestly on scale, cost, and infrastructure.

Ryan··7 min readaws-s3ziptutorials

Someone on your team asks for a "download all" button next to a folder of S3 objects — a customer's exported reports, a gallery of uploaded photos, a batch of generated invoices. It sounds like a small feature. Then you go looking for the S3 API call that returns a prefix as one archive, and it doesn't exist.

S3 stores and serves individual objects. It has no operation that zips a prefix server-side, no "archive this folder" button in the console, and no way to hand a user a single link that resolves to many files. Every "download entire bucket as ZIP" feature is something someone built on top of S3, not something S3 does for you.

There are a handful of ways to build that feature, and they trade off very differently depending on who's downloading, how much data, and how often. Below are four, roughly in order of "least infrastructure, most limited" to "most infrastructure, most capable" — plus where a browser-side ZIP library like Eazip fits into that spectrum.

The comparison

ApproachWorks forBreaks atInfra neededCost notes
aws s3 sync / consoleYou or an admin, one-offNot usable by app users at allAWS CLI or console accessStandard S3 egress
Presigned link per fileA handful of files, technical usersMore than ~10–20 files; browsers throttle concurrent downloadsOne backend endpointStandard S3 egress
Server-side ZIP (Lambda/backend)App users, moderate archive sizesLambda's 10 GB /tmp and 15-minute timeout; large fan-out on your infraCompute + temp storage + deliveryDouble egress: S3→server→user
Browser ZIP (Eazip)App users, no server ZIP infraVery large archives need Cloud offload; requires bucket CORSPresigning endpoint onlySingle egress: S3→user (or S3→Cloud→user)

Way 1: aws s3 sync or the S3 console

If the person who needs the files has AWS credentials, this is the fastest path and requires zero code:

aws s3 sync s3://your-bucket/reports/2026-Q2/ ./2026-Q2/

The console also lets you select multiple objects and download them, though it zips them client-side in the browser tab and has its own size and count ceilings that AWS doesn't document precisely and has changed over time.

Verdict: fine for internal use — an admin pulling a customer's data for support, an engineer grabbing logs. Not a feature you can ship to end users, because it requires AWS credentials and a terminal. If the ask is "give our customers a download-all button," skip straight to Way 2 or later.

Way 2: A presigned URL per file

The next step up is generating a presigned GetObjectCommand URL for each object and letting the browser fetch them:

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: 'us-east-1' });

export async function presignedUrlsForPrefix(bucket: string, prefix: string) {
  // list objects under prefix, then for each:
  return getSignedUrl(s3, new GetObjectCommand({ Bucket: bucket, Key: prefix }), {
    expiresIn: 900,
  });
}

The frontend then either opens each URL in sequence or triggers <a download> for each one.

Verdict: works for two or three files. Past that it falls apart in ways that have nothing to do with your code: browsers cap simultaneous downloads per origin, a burst of <a download> clicks gets treated as a popup storm by some browsers, and the user ends up with a dozen separate files in their Downloads folder instead of the one thing they asked for — "all of it." There's no archive, no single artifact, and no progress indicator for the batch as a whole.

Way 3: ZIP the files on a server

This is the traditional answer: a backend job (often a Lambda function, sometimes a long-running server process) streams each object out of S3, pipes it into a ZIP writer, and either streams the result back to the client or uploads it somewhere and returns a link.

// AWS Lambda handler, simplified
import archiver from 'archiver';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';

export const handler = async (event) => {
  const archive = archiver('zip');
  const s3 = new S3Client({});

  for (const key of event.keys) {
    const object = await s3.send(new GetObjectCommand({ Bucket: event.bucket, Key: key }));
    archive.append(object.Body, { name: key });
  }

  archive.finalize();
  // stream `archive` to the response, or pipe to an S3 upload
};

Verdict: this genuinely works, and for a long time it was the only real option. But you inherit Lambda's hard limits as your product's limits: /tmp is 512 MB by default and configurable only up to 10 GB, execution is capped at 15 minutes with no way to request more, and memory tops out at 10,240 MB — which also happens to set your available CPU, since Lambda allocates CPU proportionally to memory. A large export that needs more than 10 GB of scratch space or more than 15 minutes to assemble simply doesn't fit in one invocation; you have to shard it yourself. You're also paying for egress twice — once from S3 into your Lambda or server, and again from your server out to the user — and you own the scaling, retry, and cleanup logic for archive jobs that fail partway through. Running this outside Lambda, on a container or VM, removes the 15-minute and 10 GB ceilings but replaces them with "you now operate a fleet of workers that hold customer data in memory or on disk."

Way 4: ZIP in the browser with a streaming library (Eazip)

The fourth option skips the server-side archive step entirely. The backend's only job is to list the objects and hand back presigned URLs — the same list-and-presign code as Way 2 — and the browser streams each object and writes it directly into a ZIP as the bytes arrive, using @eazip/core:

import { createZip } from '@eazip/core';

const files = await fetch('/api/exports/bucket-prefix', {
  credentials: 'include',
}).then((r) => r.json()); // [{ url, filename }, ...]

const result = await createZip({ files, zipName: 'export.zip' });
result.download();

Verdict: there's no ZIP infrastructure to run or scale, and S3 egress only happens once — straight to the person who asked for the download, instead of through an intermediate server. The tradeoffs are real, though: the bucket needs a CORS rule allowing the app's origin, since the browser is fetching objects directly; it's a third-party dependency you're trusting to handle the archiving correctly; and a Local job (the default) is bounded by the browser tab's memory and by the tab staying open, so a multi-gigabyte or multi-thousand-file export should use strategy: 'cloud' to move the work off the visitor's device. The DIY version of this approach — building your own in-browser zipper on top of a library like JSZip — hits a harder wall than Eazip's Local mode does: JSZip builds the whole archive in memory before it can be saved, so it runs out of headroom well before a modest gallery export finishes, which is the specific problem streaming ZIP writers exist to avoid.

Which one to pick

  • You, once, with AWS access: aws s3 sync. Don't build anything.
  • A few files, technical audience, no archive needed: presigned links per file.
  • You already run backend infrastructure for large jobs and archives regularly exceed what fits in a Lambda invocation: a server-side ZIP job, sized for your worst case.
  • You want a "download all" button for regular app users, don't want to operate ZIP infrastructure, and can add a CORS rule to the bucket: ZIP in the browser, with Cloud for anything multi-gigabyte or reload-sensitive.

Frequency matters as much as size. A once-a-quarter admin export tolerates a slower, more manual path that a customer-facing "download all" button used daily cannot.

Further reading

The full implementation — listing a prefix with pagination, presigning, bucket CORS, and the Eazip Cloud switch for larger jobs — is in Download an Entire S3 Bucket or Prefix as a ZIP. For the Local-versus-Cloud tradeoff in more detail, see When to use Eazip Cloud.

FAQ

How do I download an entire S3 bucket as a ZIP file?

There's no single-step way, because S3 doesn't produce archives itself. You either sync the bucket locally with the AWS CLI (for yourself), or build a step that lists the objects, gets access to each one via presigned URLs or server-side credentials, and archives them — either on a server (Lambda or your own backend) or in the browser with a streaming ZIP library.

Can I download multiple S3 files at once without zipping them?

Yes, with presigned URLs per file, but it only works for a handful of files before the browser's per-origin download limits and the lack of a single combined artifact make it impractical. Past a few files, users expect one ZIP, not a folder of separately-downloaded files.

What's the fastest way to let users download an S3 folder as a ZIP?

For occasional, small folders, presigned links or a server-side ZIP job are simplest to reason about. For a recurring "download all" feature at meaningful scale, browser-side ZIP streaming avoids standing up and scaling archive infrastructure, at the cost of needing bucket CORS and, for very large exports, an offload path like Eazip Cloud.

Does zipping S3 objects cost extra on top of storage?

Yes — every approach still incurs S3's standard data transfer pricing when an object is read out of the bucket, roughly $0.09/GB after the first 100 GB/month in US regions (other regions and tiers differ). Server-side zipping pays that egress twice, once into the server and once out to the user; zipping directly in the browser or via Eazip Cloud pays it once.