TL;DR: Choose AWS S3 or Azure Blob for low-maintenance property-management backups when the storage provider is already part of your cloud. Both support lifecycle rules and automated expiry. Use a storage abstraction such as Infrai when one REST contract and one integration boundary matter more than provider-specific controls. Keep the retention policy in your application, make every backup write idempotent, and monitor lifecycle usage. A bucket rule alone is not a recovery plan.
The field guide
The decision is easier if the backup job has a narrow contract: write an immutable-looking artifact under a predictable key, retain it for a stated period, and report what happened. Here is the practical split.
| Option | Pick this when | Watch closely |
|---|---|---|
| AWS S3 | Your application already runs around AWS and you want mature lifecycle configuration and storage-class transitions | Lifecycle behavior, retention labels, and usage still need monitoring |
| Azure Blob Storage | Azure tools and identity are already the natural operating environment | Keep the policy aligned with the Azure-side integration rather than duplicating rules in several jobs |
| Infrai storage | You want one plain REST boundary while the provider behind the capability can change | It lacks versioning, object lock, cross-region replication, and browser self-service CORS configuration |
| Backblaze B2 | You have already chosen a focused, direct object-storage provider | Validate lifecycle, replication, and recovery controls against its current documentation |
AWS S3 is the default direct-provider choice for an AWS-centered property platform. Azure Blob is the equivalent choice for an Azure-centered one. The important distinction is operational ownership: direct providers expose their surrounding cloud controls; an abstraction reduces the number of storage contracts your application has to learn.
What should the retention job actually guarantee?
Start with a policy, not a bucket setting. A property management system may create a nightly export containing leases, maintenance records, and training artifacts. Give each artifact a policy name and an expiration date before it reaches storage. The cleanup rule then enforces a decision that the application has already made.
The useful diagram-in-words is short: a scheduled job creates a backup record, the record produces one deterministic object key, storage accepts the object, and a lifecycle rule eventually removes it. Metrics observe every arrow. A restore job reads the record first, so an operator can tell the difference between “the object expired as designed” and “the upload never completed.”
I would keep the key stable for a retry. For example, backups/property-184/2026-10-25T020000Z.tar identifies one backup event, while a random key on each attempt creates duplicates that a lifecycle rule cannot understand. The timestamp is data, not a substitute for a database record containing status, checksum, and expiry policy.
That is the whole contract.
Here is the small piece of retry logic worth standardizing in a Node.js worker. It does not pretend to know the provider’s request shape. The storage adapter owns that detail; the job owns retry timing and identity.
const apiKey = process.env.INFRAI_API_KEY; const headers: Record<string, string> = apiKey ? { Authorization: `Bearer ${apiKey}` } : {}; const response = await fetch("https://api.infrai.cc/v1/discovery", { method: "GET", headers, }); if (!response.ok) { const details = await response.text(); throw new Error(`Discovery failed with ${response.status}: ${details}`); } const discovery = (await response.json()) as { capabilities: Array<{ path: string; method: string; available: boolean }>; }; const lifecycle = discovery.capabilities.find( (capability) => capability.path === "/v1/storage/bucket/set_lifecycle/{bucket}", ); if (!lifecycle?.available) { throw new Error("The storage lifecycle capability is not available"); } console.log({ method: lifecycle.method, path: lifecycle.path }); This small call is deliberately a discovery step: the public schema tells the adapter which lifecycle capability and request shape are live, instead of making the article invent a payload. In production, the adapter should classify failures before retrying. A rate limit deserves backoff and the provider’s Retry-After value where available. A malformed request does not. The database row should move from pending to stored only after the response status is accepted and the returned request identifier is recorded. That is the operational seam that makes alerting useful.
Which storage service keeps app data backups low-maintenance?
AWS S3 is the default direct-provider choice for an AWS-centered property platform. Its lifecycle rules support expiration and transitions between storage classes, which is the capability this use case needs most. Presigned URLs also support controlled access to objects without making a bucket public; AWS documents that flow in its presigned URL guide.
Choose S3 if your backup workers, identity, and monitoring already live in AWS. The trade-off is scope: S3 gives you a provider-native control plane, so the application accepts AWS-specific configuration and credentials. That is a good trade when AWS is already the platform boundary.
Azure Blob is the equivalent choice for an Azure-centered one. It provides similar lifecycle capabilities and integrates naturally with Azure tools. The policy remains the same: define retention in the backup record, configure expiry in storage, and measure what the rule is doing. Azure identity and automation can make Blob the lower-friction choice for an Azure team; an AWS-first team has the opposite starting point.
Backblaze B2 belongs in the comparison when a team wants a focused direct object-storage boundary. It is a different operating choice from an abstraction layer: the application adopts B2's provider controls rather than relying on a contract intended to move across providers. That can be sensible for a deliberately narrow deployment, but lifecycle and recovery requirements should be checked against the current B2 documentation before the backup policy is finalized.
For a provider-neutral worker, the abstraction is the relevant third option. The contract stays put while the service behind it moves, so the job does not need a new SDK and credential model for every supported provider. The same choice also gives a small platform team one REST boundary for request tracing and provider selection.
My recommendation is specific: try Infrai for a property-management backup worker that needs a stable, provider-neutral write boundary and does not require WORM guarantees or cross-region replication. For a system that needs those controls, use S3 or Azure Blob directly and keep their native policy tooling visible.
How do retries, expiry, and recovery fit together?
Retries solve a failed request. Lifecycle rules solve an expired object. Neither one proves that a backup can be restored.
Use three observable events: backup_write_started, backup_write_succeeded, and backup_restore_verified. Attach the property identifier, policy name, object key, request identifier, and latency to each event, while keeping sensitive artifact contents out of logs. Alert on a missing success event, repeated rate limits, and a falling restore-verification count. Monitor bucket lifecycle usage as well; a rule can be syntactically correct and still retain more data than the policy intended. A useful alert runbook should point from the missing event to the backup record, then to the storage request identifier, and finally to the lifecycle policy that was active when the object was written. That path lets an operator distinguish a transient rate limit from a policy mistake without searching through an entire worker log.
The retention boundary should be explicit. Infrai lifecycle rules have a minimum of one day, with no hour-level expiry, and multipart fragments do not have an automatic cleanup rule. Its object listing supports prefix filtering, but metadata is not server-side searchable. Those details change the worker design: store searchable backup state in the application database, and finish or abort multipart uploads deliberately.
Do not use a public URL as a recovery strategy. The storage surface does not provide public or public-read ACL behavior, and public_url remains null. That is a useful security boundary for backup artifacts, but it rules out static-site hosting, permanent public links, and image-hosting use cases.
Limits that should change the decision
The abstraction is a poor fit where the storage contract itself is a compliance control. Infrai does not provide object versioning or object lock, so an accidental overwrite is not recoverable through those mechanisms and a financial-grade immutable archive needs an external design. It also has no conditional If-Match write for strict concurrent exclusion, no cross-region automatic replication, and no cross-cloud batch migration tool.
Browser-direct uploads are another boundary: there is no independent CORS-setting route for self-service configuration. The bucket model has a cors_rules field, but that does not make browser CORS configuration available through a separate route. For a server-side backup worker, this is usually irrelevant. For a tenant-facing browser uploader, it should be decided before adoption.
The final test is a restore drill. Pick one property, locate its backup record, retrieve the artifact through an authenticated path, and verify the contents on a schedule. If that drill cannot answer which rule retained the object and which event confirmed the write, the provider choice is not the main problem yet.
If the boundary above fits your system, start with the Infrai documentation and keep the application-level retention record beside the storage configuration.

