Learning objectives
After reading this article you will be able to:
- Understand the line items behind the bill for storing user files
- Estimate a year of uploads with a step-by-step calculation
- Cut the cost with compression, lifecycle rules and caching
Profile pictures, chat attachments, PDF receipts, product images: almost every app ends up storing files its users send. At first the volume is small and the bill barely shows. The catch is that uploads pile up. Unlike traffic, which rises and falls, whatever you stored is still there next month, and the month after. That is why it pays to estimate a year of uploads before you pick where to keep them.
The numbers in this article are hypothetical, chosen to keep the math easy, and are in US dollars. They are not any provider's prices; real prices change, vary by region and are on the official pages, so check the official AWS and Backblaze pages before you decide.
What makes up the cost of storing uploads?#
In object storage, which is where uploads usually live (see what is object storage), the bill has up to three lines:
- Storage: a price per GB stored per month. This is the line that grows on its own, because each month adds new files on top of the old ones.
- Requests: every upload (PUT) and every read (GET) may be billed, usually per thousand. It matters in apps with many small files, such as thumbnails.
- Egress: a price per GB that leaves storage for the internet when someone opens the file. With several providers, it is the most expensive line. The article on egress fees explains how it works.
Outside storage, there is processing (generating thumbnails uses your server's CPU) and, if you use one, the CDN in front of public files.
How do providers bill each line?#
Two billing models, with the hypothetical values used in the calculation further down:
| Item (hypothetical value) | Provider A: bills each line | Provider B: egress allowance |
|---|---|---|
| Storage | $0.02 per GB/month | $5 per TB/month |
| Uploads (PUT, COPY, POST, LIST) | $0.01 per 1,000 | No charge |
| Reads (GET) | $0.001 per 1,000 | No charge |
| Egress to the internet | First 100 GB/month at no charge, then $0.10 per GB | No charge up to 2 times your stored data, then $0.02 per GB |
Both designs exist in the market. S3 bills each line separately and offers cheaper classes for rarely accessed files, like provider A. B2 has cheaper storage per GB and bundles requests and part of the egress, like provider B. Neither is cheaper in every case: the answer depends on how much you store and how much gets downloaded.
How do you estimate your upload volume?#
Do the math with four numbers:
GB per month = uploads per month × average size × number of stored versions
GB stored in month N = GB per month × NWhat moves the bill most is the number of versions. Many apps store the original upload, a display version and a thumbnail. A phone photo is a few MB; the same image resized to WebP for display is a few hundred KB, and the thumbnail a few dozen.
Hypothetical example: an app with 5,000 active users, each uploading 10 photos per month. Each upload produces a 3 MB original, a 300 KB display version and a 40 KB thumbnail. Every month the app serves 1 million views of the large version and 3 million thumbnails.
The script below calculates the bill for month 12 and runs on any recent Node.js version:
// uploads-cost.mjs: node uploads-cost.mjs
// Hypothetical example in USD: provider A bills each line; provider B has an egress allowance.
// These are not any provider's prices; swap in the official ones before relying on the result.
const uploadsPerMonth = 50_000; // 5,000 users x 10 photos
const months = 12;
const mbPerUpload = 3 + 0.3 + 0.04; // original + display version + thumbnail
const putsPerUpload = 3;
const getsPerMonth = 4_000_000; // 1 million views + 3 million thumbnails
const gbDownloadedPerMonth = 1_000_000 * 0.0003 + 3_000_000 * 0.00004; // 420 GB
const gbStored = (uploadsPerMonth * mbPerUpload * months) / 1000;
const providerA = {
storage: gbStored * 0.02,
requests:
((uploadsPerMonth * putsPerUpload) / 1000) * 0.01 +
(getsPerMonth / 1000) * 0.001,
egress: Math.max(0, gbDownloadedPerMonth - 100) * 0.1,
};
const providerB = {
storage: (gbStored / 1000) * 5, // $5 per TB-month
requests: 0, // requests are not billed
egress: Math.max(0, gbDownloadedPerMonth - 2 * gbStored) * 0.02, // allowance of 2x stored
};
console.log(`Stored in month ${months}: ${gbStored.toFixed(0)} GB`);
for (const [name, c] of Object.entries({ "Provider A": providerA, "Provider B": providerB })) {
const total = c.storage + c.requests + c.egress;
console.log(
`${name}: storage $${c.storage.toFixed(2)}, ` +
`requests $${c.requests.toFixed(2)}, ` +
`egress $${c.egress.toFixed(2)}, total $${total.toFixed(2)}`,
);
}The result, with about 2 TB stored in month 12:
| Provider | Storage | Requests | Egress | Month 12 total |
|---|---|---|---|---|
| Provider A | $40.08 | $5.50 | $32.00 | $77.58 |
| Provider B | $10.02 | $0.00 | $0.00 | $10.02 |
These numbers come from the hypothetical values, before taxes and currency conversion. At provider B, egress is free of charge here only because the downloaded volume (420 GB) is less than 2 times the stored amount; in month 1, with only 167 GB stored, the allowance would be 334 GB and the remaining 86 GB would be billed.
What weighs most: storage, requests or egress?#
Run the same script without keeping the original (change mbPerUpload to 0.3 + 0.04). Storage in month 12 drops from 2,004 GB to 204 GB, and the provider A total drops from $77.58 to $41.58. Egress stays at $32.00, because visitors never downloaded the original.
The example leaves three takeaways:
- The original is the biggest storage cost. It takes almost 90% of the space in the example and is never shown to anyone.
- Egress follows views, not stored volume. In a popular app it easily overtakes storage, with providers that charge for egress.
- Requests rarely dominate, unless you have millions of tiny files or repeated reads without a cache.
What hidden costs show up?#
- Orphaned files. A user deletes their account or changes their photo, and the old file stays in the bucket forever. Delete the object together with its database record.
- Incomplete multipart uploads. In large uploads split into parts, the parts of an abandoned upload keep taking up space. AWS recommends a lifecycle rule with
AbortIncompleteMultipartUploadto delete them after a few days. See how to upload large files. - Versioning turned on. With versioning, overwriting a file does not delete the previous version, and every version is billed.
- Minimum duration in cold classes. Infrequent-access classes charge a minimum storage time (30 days on S3 Standard-IA, 90 to 180 days on the Glacier classes). Moving files there that will be deleted soon can cost more.
- Images in the database or on the server disk. Storing bytes in the database makes disk and dumps more expensive; storing them on the app's local disk ties the files to one machine. Compare in images in a database vs object storage and object storage vs local disk.
- Hotlinking. Another site using the direct link to your images creates egress on your bill.
How do you reduce the cost of storing uploads?#
In order of payoff:
- Resize and compress on upload. Generate the display version in WebP or AVIF with a maximum width, and decide whether the original really needs to stay.
- Limit file size and type before accepting the upload. A 10 MB cap and a list of allowed extensions keep 2 GB videos out of a profile picture field.
- Use lifecycle rules to delete temporary files, move originals to a cheaper class and clean up incomplete uploads. See object lifecycle rules.
- Put a cache in front of public files, with a long
Cache-Controlon files that never change, so egress from storage happens once per file instead of once per visit. - Upload straight from the browser to storage with a presigned URL, so the file never passes through your server or uses its bandwidth.
- Pick the provider by the line that weighs most for you. Lots of views call for cheap egress; lots of stored volume calls for cheap storage. The article on object storage cost compares pricing models.
How to do this on Square Cloud#
On Square Cloud, uploads can go to Blob Storage, an object storage with an S3-compatible API (aws-cli, rclone, boto3 and the AWS SDKs), a REST API and a dashboard. It charges no egress or ingress fees, so the egress line from the example doesn't exist here: downloads carry no bandwidth charge. The limit that does apply is a per-plan rate limit on S3 requests.
Every plan includes a Blob quota, from 5 GB to 250 GB. Hobby, from R$12.49 per month, includes 5 to 15 GB. Public files are served by a global edge CDN; private files open through a temporary link of up to 24 hours or a revocable share link of up to 30 days.
Per-folder rules cover much of this article: maximum size, allowed extensions, expiration, caching and automatic cleanup. Files can be up to 10 GB, and multipart is only required above 100 MB. See the details on the Blob Storage page.
Blob Storage
S3-compatible object storage with no egress fees
FAQs
How much does it cost to store 1 TB of images per month?
Storage alone is the volume in GB times the provider's price per GB-month. Assuming $0.02 per GB-month (a hypothetical value), 1 TB (1,000 GB) comes to $20 per month; at a hypothetical $5 per TB-month, it would be $5. Requests and egress are billed on top.
Is it cheaper to store images in the database?
Almost never. Database disk usually costs more per GB than object storage, images bloat your dumps and every read goes through the database. The usual pattern is to keep the file in object storage and only its key in the database.
What is the biggest part of an uploads bill?
It depends on usage. In an app with many views, egress is often the largest line with providers that charge for it. In an app that keeps large originals and rarely shows them, accumulated storage dominates.
Should I keep the original photo the user uploaded?
Only if you need it, for example to reprocess later. A phone photo is a few MB, while a WebP display version can be a few hundred KB. Dropping the original or moving it to a cheaper storage class removes most of the storage cost.
More in Storage
See all 15 articles- What is a presigned (signed) URL?A presigned URL is a temporary link to a private file that works without login or credentials. See how it works, its limits and when to use it.
- What is object storage?Object storage keeps files as objects with a key and metadata, accessed over an HTTP API. See how it works, what it is for and where its limits are.
- What does 'S3-compatible' storage mean?S3 is Amazon's object storage and its API became the industry standard. Learn what S3-compatible means, what differs between providers, and how to connect.
- Uploading large files: multipart and resumable uploadsMultipart upload splits a large file into parts that are sent in parallel and retried on their own. See the Python walkthrough, how to resume, and common mistakes.