Setting up S3 storage: where the web application breaks
Setting up S3 storage almost never breaks at the moment of connection — it breaks later, when the application starts working with a real stream of files.
The first symptoms usually look harmless: a user uploaded a document, the interface showed success, but the object is missing from the bucket. Or the opposite — the file is there, but the application can't find it and serves a broken link.
The reason is that writing to object storage and writing metadata to the database are two separate operations, and between them there is always a window in which the state diverges.
Then timeouts come into play. A large file or a slow channel hits the limit on the application side before the storage has time to confirm receipt — and the client gets an upload error for a file that actually did upload.
A retry creates a duplicate, and if the application retries aggressively, then several copies of the same document. A separate story is object metadata: content type, access rights, lifespan.
If they aren't set explicitly, the browser downloads the image instead of displaying it, and temporary links live longer than they should.
And the most expensive class of failures is the leakage of access keys. When a key is stored in code, ends up in logs, or gets shipped to the client side, an outsider gains full access to the bucket: read, overwrite, delete. As a rule, this is discovered after the fact, not through monitoring.
Let's break down how this is closed off: where to draw the boundary between writing the object and the metadata, how to configure timeouts and retries so they don't multiply duplicates, and how to separate permissions so that the leakage of one key doesn't open up the entire bucket.
What a correct storage configuration delivers
A correct storage configuration shows up in three measurable things: file delivery time, behavior under peak load, and the number of support requests.
If object storage is configured properly, static assets and user files are served directly, bypassing the application, and the server handles only logic. This relieves the load on compute resources and removes the bottleneck that causes pages to "hang" as traffic grows.
File delivery speed is determined by how caching headers and delivery through the external edge are configured.
When the browser receives correct object caching rules, repeat visits don't pull images and documents again — page load time drops, and the channel isn't clogged with unnecessary traffic.
Peak load stops being a problem because static asset delivery is distributed to the storage rather than the application.
Storage availability is a separate layer of work: redundancy, access rights verification, object lifecycle control. When retention and deletion policies for outdated files are configured, the registry doesn't grow uncontrollably, and the cost of storage remains predictable.
We separately test failure scenarios: what happens if the storage is unavailable, how the application behaves when delivery fails, and whether there is a fallback path.
Reduction in incidents is the result of all this work. Most support requests about "images won't load," "the file won't download," or "the site is slow" are related specifically to storage configuration, not to application code.
When access rights, caching, and delivery are configured correctly, these requests disappear, and the team focuses on product development instead of putting out the same types of failures.
Comparing storage options: cloud, MinIO, hybrid
The choice between cloud S3, self-hosted MinIO, and a hybrid scheme is not a matter of taste but a calculation along three axes: who is responsible for data durability, how much volume growth costs, and how fast access is needed. Below is a comparison by the criteria that actually affect operations.
| Option | When it fits | Limitations | Infrastructure requirements |
|---|---|---|---|
| Cloud storage (S3-compatible) | Product launch, unpredictable load, no in-house operations team | Charges for outbound traffic and requests, provider dependency, latency to the region | Minimum: only API access and keys. Redundancy is on the provider's side |
| Self-hosted MinIO | Sensitive data, predictable volume, requirements to keep files within your own perimeter | Requires disks with headroom, monitoring, backups, and on-call duty; fault tolerance is built by you | A cluster of several nodes, a dedicated volume for data, replication and lifecycle policies |
| Hybrid scheme | Hot data close to the application, archive and backups in the cloud | More complex routing and access rights, synchronization between perimeters is needed | Local MinIO + cloud bucket, a unified versioning and lifecycle policy |
The key limitation of S3 as an interface is the same for all three options: it is not a file system but object storage. Overwriting an object in full, the absence of locks, and expensive rename operations dictate how to design the key structure and versioning. If the application was written for a local disk, some scenarios will have to be reworked.
In practice, the hybrid more often wins: the hot layer on MinIO provides predictable latency and independence from the network link, while the cloud covers the archive, backups, and spikes.
Switching between perimeters is done at the client configuration level, without rewriting code — but only if replication and versioning policies are defined in advance.
Stages of S3 storage implementation and configuration
We start storage implementation with an audit: we look at where the application serves static assets and user files from, who accesses them and with which keys, and how permissions are structured.
At this stage, bottlenecks come to light — for example, files are read through the backend, and every request for an image occupies an application worker process.
Next, we design the scheme: bucket layout, access policies, object lifecycle, and separation of public and private content.
- Infrastructure audit. Inventory of current storage, access points, and data volumes, assessment of read and write load.
- Scheme design. Structure of buckets and prefixes, access policies, lifecycle rules, delivery via a separate domain.
- Storage configuration. We set up an S3-compatible service, enable versioning, encryption, and backup, and configure application keys.
- Data migration. We transfer existing files with integrity checks and link preservation — we catch broken paths before switching traffic.
- Access testing. We check read, upload, and delete scenarios, role permissions, behavior when a node is unavailable, and correct delivery through the content delivery network.
- Switching and monitoring. We move the traffic, monitor latency and errors, and roll back to the previous scheme if there are deviations.
- Documentation handover. We give the team a description of the scheme, policies, and instructions for typical operations.
After the switch, the application stops proxying files through the backend — static assets and attachments go directly to storage, and worker processes are freed up for logic.
Separately, we establish rotation rules: old object versions are cleaned up automatically, so the registry volume does not grow uncontrollably, while rollback to a previous file version remains possible.
Access policies, object lifecycle, and roles
Permissions in object storage are described by policies in JSON format: who, to which prefixes, and which actions are allowed. We do not grant an application full access to the bucket — each service has its own role with the minimum necessary set of operations.
The avatar upload service sees only the avatars/ prefix, the reporting service sees only reports/ and read-only. If one component is compromised, the rest of the data remains inaccessible.
Permission separation is built on roles, not on static access keys. The application receives temporary credentials through an access management service, keys are rotated automatically, and a leaked key stops working after a defined interval.
Public access to the bucket is closed at the policy level — files are served through signed links with a limited lifetime, so direct listing of contents is impossible.
The object lifecycle removes manual cleanup. A rule moves objects older than 30 days to a cold storage class, and after 180 days deletes them — storage costs stop growing linearly.
For versioned buckets, a separate rule cleans up old versions and incomplete multipart uploads: they are not visible in the listing, but they take up space and show up in bills.
We configure data archiving so that rollback remains possible. Before deletion, objects go to a separate archive bucket with a stricter access policy, and we put a lock against accidental deletion on the bucket itself.
We store access logs in the same perimeter — they show who accessed objects and when, which satisfies audit requirements without a separate system.
We describe everything in declarative configurations, not in manual settings through a panel. We deploy storage on MinIO or a compatible service — the configuration is transferred between environments without rewriting, and changes go through review like ordinary code.
What the client receives after storage setup
S3 storage setup ends not with launching the service, but with handing the team a set of artifacts that allow the system to be operated, extended, and restored without the original author. Below is what is included in the delivery.
- Storage configurations — bucket parameters, versioning, lifecycle rules (how many object versions and backups we keep, what goes to archive), upload size limits, and quotas. All as files in the repository, not "manually in the panel."
- Access policies — roles and permissions: who reads, who writes, who manages the bucket. Public access is closed by default; access to objects is granted via signed links with a limited lifetime.
- Integration schemes — how the application communicates with the storage: file uploads, content delivery, working with temporary links, behavior when storage is unavailable. With a data flow diagram and failure points.
- Backup and recovery regulations — what we copy, how often, where copies are stored, how their integrity is verified. With a step-by-step recovery instruction and measurement of time to return to operation.
- Team instructions — typical operations: connect a new bucket, grant access to a contractor, investigate an upload incident, roll back an object to a previous version. Short, with commands and expected results.
- Monitoring and alerts — metrics for storage fill, access errors, delivery latency. Alerts are configured so that a problem arrives before user complaints.
- Documentation — a single document: architecture, decisions made and why exactly so, known limitations, contacts of responsible persons.
The set is self-sufficient: a new engineer on the team brings up the environment and reproduces the configuration from the documentation, without parsing someone else's settings from logs. Describe the task — we will return with an estimate and a work plan.
How S3 differs from MinIO and what to choose as load grows
S3 is not a product but a protocol for accessing object storage: a set of operations on objects and buckets, unified across all compatible implementations.
An application works with storage through a software interface rather than through a file system, so it does not care who exactly answers the requests — a cloud service or your own cluster.
It is precisely this interface compatibility that allows you to change storage without rewriting the code for uploading, serving, and processing files.
MinIO is an independent implementation of the same protocol that you deploy on your own servers. It gives you full control over data placement, no lock-in to a single vendor, and predictable behavior under peak loads.
The price for this is operations: disks, replication, backup, updates, and monitoring fall on your team. Cloud S3 removes these concerns but ties you to storage and traffic tariffs that grow along with data volume.
The choice as load grows is determined not by volume as such but by the access profile. If data is rarely read and the volume is measured in terabytes, storage with a cheap cold tier and object lifecycle is more cost-effective.
If low latency on hot files matters, what decides is the proximity of storage to the application and the presence of a cache at the edge.
With a sharp increase in the number of concurrent uploads, check how the implementation handles parallel streams and whether it can scale by adding nodes.
The practical criterion is simple: as long as volumes are moderate and the team does not want to maintain infrastructure, cloud storage is cheaper in terms of labor costs.
When traffic and volume become a significant line item and the requirements for control over data grow, your own implementation on a compatible protocol pays off.
Migration between them is possible without rewriting the application — thanks to interface compatibility — but it requires a well-thought-out data migration and access rights verification.
Case study: how file delivery time was cut by 3x
The client is a service with user documents and photos: files were served directly from application servers, bypassing object storage. The load on disks grew along with the volume of data, and during peak loads the download response time reached 4-5 seconds.
At the same time, errors were accumulating: some requests failed due to timeout, and if a single node went down, files became unavailable until recovery.
What was changed
We deployed a MinIO cluster in distributed storage mode and migrated the application to work through an S3-compatible interface. Files no longer pass through the application servers: the client receives a temporary signed link and retrieves the object directly from storage.
We configured lifecycle policies — infrequently requested documents automatically move to a cold class, and versions older than a specified period are deleted.
Metrics before and after
| Metric | Before | After |
|---|---|---|
| Average file delivery time | 4-5 s | 1.4 s |
| Share of requests with errors | 3.1% | 0.2% |
| Load on application server disks | 100% | 12% |
| Fault tolerance | single node | node loss is unnoticeable |
We separately addressed the rollback issue: object versioning preserves previous revisions, so accidental overwriting of a document can be restored without contacting support. Backup now happens at the storage level — replication between nodes instead of daily full exports.
A month after the migration, storage volume grew by a third, but disk costs did not change: the cold class and cleanup of old versions kept growth in check.
The application servers were offloaded so much that their number was cut in half — they no longer spend resources on serving static content.
Common technical questions about storage configuration
Storage is the part of an application where a mistake in access permissions or lifecycle policy doesn't show up immediately, but months later: either as a file leak or as an inflated storage bill.
That's why below are answers to the questions that most often come up during the design and migration phase.
How safe is it to store user files in external storage?
Access to objects is closed by default and is granted through signed links with a limited lifetime — the direct object address is never exposed externally.
We separate permissions at the policy level: the upload service, the delivery service, and background tasks each have different keys with the minimum required permissions.
We enable encryption on the storage side and keep an access log — it shows who read a specific object and when.
What happens to already uploaded files during the transition?
We perform the migration without downtime: first we enable writes to the new storage, then we move the accumulated volume with background tasks while verifying checksums, and only after reconciliation do we switch reads.
We preserve old addresses through redirection, so links in the database and in external systems continue to work. Rollback is possible at any stage until the source array is deleted.
Is this compatible with the current stack, and will the code have to be rewritten?
The object storage access interface is standard, so the application communicates with it through a familiar library rather than a custom-built layer. We spin up a compatible server locally — development and tests run without connecting to the production environment.
If part of the logic is already tied to a local disk, we move it to an abstraction layer so that changing storage in the future won't require edits to the business logic.
How do you control storage costs?
Lifecycle rules keep the latest versions of objects on fast access, while rarely requested ones are moved to a cold class and deleted by age. The registry stops growing indefinitely, while rollback to a previous version of a file remains possible. Costs stop depending on upload peaks — they are tied to the volume actually needed.
Describe your task — we'll get back to you with an estimate and a migration plan.
Let's discuss your storage task
We discuss the storage task concretely: what the load profile is, where files flow from and to, what already works and what breaks. To start, a description of the scenario is enough — uploading user documents, serving media, database backups, log archives.
Then we ask clarifying questions about access, volumes, and fault-tolerance requirements, and you get an estimate and a work plan with stages and areas of responsibility.
Setting up S3 storage rarely comes down to "spin up a bucket and hand out keys." You need to define the access model, the lifecycle policy, the backup scheme, and how the application will survive storage unavailability. That's why the task is broken down along engineering axes, not by price.
What we clarify in the first conversation:
- volume and nature of the data: static assets, user files, backups, logs — each has its own read and write profile;
- current state: what is already deployed, where it hurts — speed, cost, reliability, or access;
- access requirements: public serving, private objects, temporary links, application roles;
- fault tolerance and backup: geography of copies, target recovery time, acceptable data loss;
- integrations: which services and applications need to read from and write to the storage;
- constraints: regulatory requirements, where the data must physically reside, who owns the infrastructure.
Based on the conversation, we come back with a timeline estimate, a staged work plan, and architecture options — from a minimal configuration to a fault-tolerant one. You see exactly what we do, in what order, and what result you get at each step.
Then the decision is yours: approve the plan in full, take the first stage, or return to the discussion with refined inputs.
Describe the task in any convenient format — text, a diagram, or a list of requirements. We will come back with an estimate and a work plan, with no obligations on your part.







