S3 for anything that can be stored and read as a whole object, EFS when several functions or containers need the same POSIX filesystem, EBS when a single EC2 instance needs a fast persistent disk. In a serverless Node.js stack, S3 is the default and the other two are exceptions you have to justify. Lambda's ephemeral /tmp covers the scratch space in between. Here is how we draw those lines on real systems.
Object, file, or block: what actually changes?
S3 stores objects behind an HTTP API: you write and read whole blobs, with no directory tree, no file handles, no partial in-place updates. EFS is a shared network filesystem with POSIX semantics: paths, permissions, concurrent readers and writers. EBS is a raw block device that behaves like a local disk attached to a single EC2 instance.
Everything else follows from the access model. Objects are replicated behind the scenes, so S3 scales without capacity planning but cannot be mounted like a disk (tooling such as Mountpoint blurs the line, with caveats). A shared filesystem must coordinate its writers, so EFS trades some latency for shareability. A block device is fast precisely because one machine owns it.
When is S3 the right default?
Whenever data is written once and read as a whole: user uploads, images, exports, logs, backups, static assets, analytics files. S3 needs no capacity planning, can trigger a Lambda function on every write, and lifecycle rules move cold objects to cheaper storage classes automatically.
The cost shape suits serverless: per GB stored plus per request, with nothing provisioned up front (the S3 pricing page lists the current tiers). Presigned URLs let browsers upload and download directly, keeping large payloads out of your functions entirely. When object latency really matters, S3 Express One Zone offers a faster single-AZ class, though we reach for it rarely. Our rule of thumb: if a workload can be modeled as objects plus events, it goes to S3 until proven otherwise.
When does EFS earn its place?
When several compute nodes need read-write access to the same files through ordinary filesystem calls. The classic serverless case: large ML models or shared caches mounted into Lambda or Fargate, read with the standard fs module instead of being downloaded from S3 on every cold start.
// S3: an HTTP call through the SDK, no filesystem involved
const { Body } = await s3.send(
new GetObjectCommand({ Bucket: "models", Key: "v3/weights.bin" })
);
// EFS mounted on Lambda: a plain POSIX read on the mount path
const weights = await fs.readFile("/mnt/models/v3/weights.bin");
An EFS mount requires the function to run inside a VPC and to go through an access point, which is real wiring to maintain. Read latency typically sits in the low milliseconds, and throughput comes in modes (elastic, provisioned, bursting) chosen per filesystem. The cost shape is per GB actually stored plus throughput charges (details on the EFS pricing page), so a mostly idle shared filesystem stays modest while heavy streaming deserves a calculation first.
Unsure whether your Lambda functions really need that EFS mount? Describe your system: a one-page diagnosis within 48 hours.
Get my diagnosis →Where does EBS fit when you run serverless?
Not in your functions: EBS volumes attach to EC2 instances, and a Lambda function simply cannot mount one. EBS is the disk under self-managed databases, boot volumes, and any single-machine workload that needs consistent sub-millisecond block I/O at predictable IOPS.
Its cost shape is the inverse of S3: you pay for every provisioned GB, and on gp3 for any extra IOPS and throughput, whether used or not (see the EBS pricing page). Sharing is essentially one volume, one instance; io2 multi-attach exists within an availability zone but remains a niche tool. Snapshots are how EBS data usually re-enters the object world. In a serverless-first architecture, EBS only appears where you deliberately kept EC2: a self-hosted database, a stateful legacy service.
What about Lambda's /tmp?
Every Lambda execution environment includes an ephemeral /tmp, configurable from 512 MB up to 10 GB, and it is often the honest answer: scratch space for unzipping archives, resizing images, or staging a file during a single invocation, with no external service involved.
Contents can survive between warm invocations of the same environment, which makes /tmp a useful cache, but nothing guarantees it: treat everything there as disposable. Durable state belongs in S3 or DynamoDB. And if you keep fighting /tmp limits, the question is usually bigger than storage; our comparison of Lambda versus servers covers when the compute model itself should change.
S3 vs EFS vs EBS at a glance
Read the last row first. If a workload does not clearly demand shared POSIX files or a dedicated block device, it belongs on S3: object storage is the cheapest of the three to operate, the only one that is natively event-driven, and the one with the fewest moving parts.
| S3 | EFS | EBS | Lambda /tmp | |
|---|---|---|---|---|
| Access model | HTTP API, whole objects | POSIX filesystem (NFS) | Raw block device | Local filesystem |
| Sharing | Unlimited concurrent clients | Thousands of concurrent clients | One instance (io2 multi-attach aside) | One execution environment |
| Latency profile | Tens of ms per request | Low single-digit ms | Sub-millisecond | Sub-millisecond |
| Cost shape | Per GB stored + per request | Per GB stored + throughput | Per GB provisioned + IOPS | Included up to 512 MB, then per configured GB |
| Serverless fit | Native: events, presigned URLs | Good, via VPC mount | None | Built in |
| When it wins | Anything that can be an object: uploads, assets, logs, exports | Shared read-write files, large ML models on Lambda or Fargate | Self-managed databases and boot volumes on EC2 | Scratch data within a single invocation |
Two caveats apply in practice. The latency row gives orders of magnitude, not benchmarks: measure with your own payload sizes and access patterns before committing. And cost shapes matter more than headline rates: a request-heavy workload can make S3 pricier than expected, just as provisioned-but-idle volumes quietly inflate an EBS bill.
Which mistakes do we keep seeing?
Two, repeatedly. Teams mount EFS into Lambda for data that is written once and read whole, where S3 would be simpler and cheaper to operate; and teams design as if Lambda had a persistent local disk, a role EBS will never fill there.
The test for the first: do you need partial writes, file locking, or one path shared across concurrent writers? If not, S3 with presigned URLs and event notifications almost certainly fits. The second mistake usually hides a state problem: move the state to S3 or DynamoDB and let /tmp be a cache. Challenging exactly this kind of decision is part of our AWS architecture work.
Decision checklist
Before provisioning anything, walk through this list:
- Written once, read whole? S3, with lifecycle rules from day one.
- Several functions or containers sharing read-write files? EFS, mounted through an access point.
- A single EC2 instance needing a fast persistent disk? EBS, snapshotted on a schedule.
- Data that only matters within one invocation? Lambda's /tmp.
- Still hesitating? Price the access pattern, not the volume: requests and throughput decide more often than gigabytes.