Object storage vs local disk: where to save your app's files

Use the app's local disk for temporary files, caches and data only that process reads. Use object storage for user uploads, files served to the public and anything that must survive a server change.

Updated 6 min read

Learning objectives

After reading this article you will be able to:

  • Understand what changes between writing to the file system and sending to a bucket
  • Compare both on speed, persistence, scaling, cost and delivery to the browser
  • Move file uploads from disk to object storage without changing the rest of the app
On this page

The first upload in almost every project is an fs.writeFile("./uploads/..."). It works on your laptop, it works on the first deploy, and then one day your users' photos vanish after an update, or half of them return 404 once you add a second instance. The code is fine. The problem is where the file ended up.

The two most common places to keep an application's files are the app's own disk and an object storage service. There is no universal winner: each one fits a kind of file, and most mature apps use both.

Local diskBrowseruploadLoad balancerAppinstance AAppinstance BGET404Diskcat.jpgDiskemptyObject storageBrowseruploadLoad balancerAppinstance AAppinstance BBucketcat.jpgdirect
On local disk, each instance only sees its own files; with a bucket, they all write to one place.

What is the app's local disk?#

It is the file system your process sees: the project folder, /tmp, any path you write to with open() or fs.writeFile. It gives you full file semantics: open, read a range, write in the middle, append at the end, rename atomically, lock. All of it happens locally, with no network, which is why it is fast.

The weak spot is that this disk belongs to one machine (or one container). Two consequences:

  • Persistence depends on the platform. The Docker documentation is blunt: data written to the container's writable layer does not persist when the container is destroyed. Volumes and persistent disks exist to fix that, but each provider has its own rule about what survives a restart, a redeploy or a move to another server.
  • It is not shared. If you run two copies of the app, each has its own disk. A file that instance A wrote does not exist for instance B.

The Twelve-Factor App sums up the rule most platforms follow: a process's file system is a brief, single-transaction cache, and any data that needs to persist goes to a stateful backing service.

What is object storage?#

Object storage keeps files as objects inside buckets, each identified by a key such as uploads/2026/photo.webp. You talk to it through an HTTP API, usually S3-compatible: upload the whole file with PUT, read it with GET, remove it with DELETE.

It trades file semantics for other advantages. On Amazon S3, writes and deletes have strong read-after-write consistency: once a PUT returns success, any reader on any server sees the new file. Capacity grows without resizing a disk, and the file gets a URL the browser can download directly, with CDN caching, or through a presigned URL when it is private.

The cost: no partial edits. On standard S3, changing one byte means uploading the whole object again. Append exists only in one specific class (S3 Express One Zone, in directory buckets). Every operation is also a network request, with far more latency than a local disk; AWS sells a separate class just to bring latency down to single-digit milliseconds.

How do they compare side by side?#

Criterion App's local disk Object storage
Access File system (open, fs) HTTP API (PUT, GET, DELETE)
Latency per operation Very low, local One network round trip
Edit a range or append Yes No (whole object), except specific classes
Shared across instances No Yes
Survives a container or server change Depends on the platform Yes, independent of the app
Capacity Limited to the machine's disk Grows with use
Serving to the browser Goes through your app Direct URL, CDN, signed link
Typical cost Included with the server Per GB stored, sometimes per request and per egress
Good for Temp files, caches, local database, sessions Uploads, media, exports, backups

The cost row varies a lot between providers. Many also charge for outbound traffic, which adds up when you serve media; see what egress fees are.

When is the local disk the right choice?#

  • Temporary files. Resizing an image, unpacking a ZIP, building a PDF before sending it. Use the system temp directory and clean up at the end.
  • Caches you can rebuild. If the file disappears, the app recreates it. Losing a cache costs time, not data.
  • Embedded databases and single-process state files. SQLite, a WhatsApp bot's session folder, local indexes. They need writes in the middle of the file and low latency, which object storage does not offer. Here the disk must be persistent, and periodic copies elsewhere are a good idea.
  • Configuration and code. Whatever ships with the deploy lives on disk by definition.

When is object storage the right choice?#

  • User uploads. Avatars, attachments, documents. They are user data, not server data, and must survive any deploy. For the full "file in storage, key in the database" pattern, see images in the database or in object storage.
  • Files served to the public. Product images, videos, downloads. Served straight from storage or a CDN, they use none of your app's bandwidth or memory.
  • More than one instance. As soon as you put a load balancer in front of two copies of the app, local disk stops being an option for shared files.
  • Large files or growing volume. Gigabytes of media do not sit well on a disk you have to resize by hand.
  • Backups. A backup kept on the same disk it is meant to protect protects nothing.

To see where block storage (the kind of disk behind a volume) fits in, read object vs block vs file storage.

How do you move uploads from disk to object storage?#

The change is usually small: where you wrote to disk, you now PUT to the bucket, and the database stores the key instead of a local path. Here is an example in Node.js with AWS SDK v3, which works with any S3-compatible storage:

js
// npm install @aws-sdk/client-s3
import { randomUUID } from "node:crypto";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";

const s3 = new S3Client({
  region: process.env.S3_REGION ?? "us-east-1",
  endpoint: process.env.S3_ENDPOINT, // omit on AWS; set it for a compatible storage
  forcePathStyle: process.env.S3_PATH_STYLE === "true", // some compatible storages require it
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY,
    secretAccessKey: process.env.S3_SECRET_KEY,
  },
});

// Before: await writeFile(`./uploads/${name}`, data)
export async function saveUpload(data, contentType) {
  const key = `uploads/${randomUUID()}`;
  await s3.send(
    new PutObjectCommand({
      Bucket: process.env.S3_BUCKET,
      Key: key,
      Body: data,
      ContentType: contentType,
    }),
  );
  return key; // store the key in the database, not a disk path
}

Three things to watch during the move:

  1. Copy the existing files. aws s3 sync ./uploads s3://your-bucket/uploads --endpoint-url "$S3_ENDPOINT" copies what is already there. Then update the old paths in the database to the new keys.
  2. Keep the disk as scratch space. If you process the file before storing it (thumbnail, conversion), do it in the temp directory and upload only the result.
  3. Decide visibility. Public files get a direct URL; private ones open through a signed link. See public vs private files.

How to do this on Square Cloud#

On Square Cloud, both places exist. The app's disk persists across restarts and deploys: a new commit (ZIP or push) overwrites only the files it contains, and the rest stays. That is why a WhatsApp bot's authentication folder stays connected after a restart. You can also open and edit those files from the browser, without a redeploy. Each application has its own disk, though. If you use load balancing to serve one domain from several applications, they don't share files, so shared uploads belong in object storage.

Blob Storage is the platform's object storage, with an S3-compatible API: the code above works once S3_ENDPOINT points to https://s3-blob.squarecloud.app (region auto), and aws-cli and rclone handle the migration. Files can be up to 10 GB, and multipart is only required above 100 MB. Public files are served from a global edge CDN, and private ones open through a temporary link of up to 24 hours or a revocable share link of up to 30 days.

There are no egress or ingress fees, and every plan includes a quota from 5 GB to 250 GB. Folder rules set visibility, expiration and automatic cleanup, which helps keep a temp folder under control.

See the details and quotas on the Blob Storage page.

Blob Storage

S3-compatible object storage with no egress fees

Learn more

FAQs

Do files saved on a container's disk disappear?

It depends on how the platform treats the container. The Docker docs say data written to the container layer does not persist when the container is destroyed. Volumes and persistent disks change that, so check your provider's rule.

Can I use object storage like a folder?

Only partly. You read and write whole files over HTTP, with a key that looks like a path. Standard S3 has no editing in the middle of a file and no append: to change an object, you upload a new full version.

Can I run a SQLite database on object storage?

Not directly. SQLite reads and writes small pages in the middle of the file, which needs a real file system. Keep the database file on disk and send periodic copies to object storage.

Is object storage slower than disk?

Per operation, yes: every read is an HTTP request over the network. For serving files to many users it usually wins, because the browser downloads straight from it, and it can sit behind a CDN without going through your app.

Next articleSending backups to object storageSend database dumps and files to S3-compatible object storage with rclone, schedule it with cron, apply retention and test the restore. Step by step.

More in Storage

See all 15 articles