<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ethan Carter</title>
    <description>The latest articles on DEV Community by Ethan Carter (@ethan-carter).</description>
    <link>https://dev.to/ethan-carter</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3991599%2Fbb3daa8b-8625-441e-9a96-b832899e55b1.jpg</url>
      <title>DEV Community: Ethan Carter</title>
      <link>https://dev.to/ethan-carter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ethan-carter"/>
    <language>en</language>
    <item>
      <title>Self-Hosting S3-Compatible Storage on Bare Metal</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Thu, 27 Aug 2026 12:52:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/self-hosting-s3-compatible-storage-on-bare-metal-1e89</link>
      <guid>https://dev.to/ethan-carter/self-hosting-s3-compatible-storage-on-bare-metal-1e89</guid>
      <description>&lt;p&gt;You self-host S3-compatible storage on bare metal by installing a single Rust binary on a Linux server and pointing any S3 client at it. RustFS installs with one script, listens on port 9000 (S3 API) and 9001 (console), and is Apache 2.0 licensed. Single-node mode is production-ready today; multi-node clustering is still under testing.&lt;/p&gt;

&lt;p&gt;Every command below is copied verbatim from the official source cited beside it. This sandbox has no Docker daemon, so none of the commands were executed here; they are marked accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Stats&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fact&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RustFS installs with one command and runs as a systemd service on x86_64 or aarch64 Linux&lt;/td&gt;
&lt;td&gt;RustFS docs (Linux quick-start)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default S3 API port is 9000; console port is 9001&lt;/td&gt;
&lt;td&gt;RustFS GitHub README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default credentials are &lt;code&gt;rustfsadmin&lt;/code&gt; / &lt;code&gt;rustfsadmin&lt;/code&gt; and must be changed&lt;/td&gt;
&lt;td&gt;RustFS README + docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RustFS is Apache 2.0 licensed and S3-compatible&lt;/td&gt;
&lt;td&gt;RustFS GitHub README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-node mode is production-ready; distributed mode is still under testing&lt;/td&gt;
&lt;td&gt;RustFS README Feature &amp;amp; Status&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What is self-hosted S3-compatible storage?
&lt;/h2&gt;

&lt;p&gt;A self-hosted S3-compatible storage server is a program you run on your own hardware that speaks the Amazon S3 API. Applications using AWS SDKs, the &lt;code&gt;aws&lt;/code&gt; CLI, or MinIO's &lt;code&gt;mc&lt;/code&gt; can talk to it without code changes, because the bucket, object, and credential model matches S3. The difference from a cloud bucket is ownership: the disks, the network path, and the uptime are yours.&lt;/p&gt;

&lt;p&gt;RustFS is one such server, written in Rust and licensed under Apache 2.0. It exposes the S3 API on port 9000 and a web console on 9001, and it stores objects on the local filesystem. Because it is S3-compatible, the same client code that targets AWS S3 also targets a RustFS node. That compatibility is the whole point of self-hosting here: you get an S3 endpoint without renting one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why run object storage on bare metal?
&lt;/h2&gt;

&lt;p&gt;Running object storage on bare metal means installing the server directly on a Linux machine instead of in a container or a managed cloud. The appeal is control. Your data stays on disks you own, in a network you define, and the cost is the hardware plus your time rather than a per-gigabyte cloud bill that grows with every upload.&lt;/p&gt;

&lt;p&gt;The trade-off is real and worth stating plainly. You own the failure modes too. A single disk dies, the server reboots, the certs expire, and that is now your incident, not a vendor's. Bare metal suits teams that already run Linux servers, care about data residency, or have steady high-volume storage that makes a cloud bill painful. It is a poor fit if you want zero operational responsibility. Self-hosting trades a recurring fee for recurring attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before you start
&lt;/h2&gt;

&lt;p&gt;Minimum: a Linux server with systemd on x86_64 or aarch64, root or sudo access, the &lt;code&gt;unzip&lt;/code&gt; utility, outbound network access to pull the package, and open ports 9000 (S3 API) and 9001 (console) so clients and the browser console can reach the service.&lt;/p&gt;

&lt;p&gt;Plan the data disk before you install. The quick-start runs RustFS in single-node, single-disk (SNSD) mode, which stores everything under &lt;code&gt;/data/rustfs0&lt;/code&gt; with no redundancy. That is fine for evaluation and dev, but a single disk has no spare. For anything you would hate to lose, attach a second disk, schedule filesystem or object-level backups, or wait for distributed mode to leave testing. The installer lets you change the data path and ports at install time, so decide the layout now rather than after data accumulates.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you install RustFS on bare metal?
&lt;/h2&gt;

&lt;p&gt;Run the official one-line installer. It downloads the binary, installs it to &lt;code&gt;/usr/local/bin/rustfs&lt;/code&gt;, registers a systemd service, starts it, and stores data under &lt;code&gt;/data/rustfs0&lt;/code&gt; by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://rustfs.com/install_rustfs.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bash install_rustfs.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[sourced from RustFS docs (Linux quick-start), NOT EXECUTED IN CI]&lt;/p&gt;

&lt;p&gt;After it finishes you get a summary with the service port (9000), console port (9001), and data directory, plus a security warning to replace the default credentials. Open &lt;code&gt;http://&amp;lt;server-ip&amp;gt;:9001&lt;/code&gt; in a browser to reach the console, then log in with the access and secret keys you set. The same server answers S3 API calls on 9000. From here you can create a bucket in the console or from the command line, which the next sections cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you run RustFS with Docker instead?
&lt;/h2&gt;

&lt;p&gt;If you prefer a container, the Docker image gives you the same server with no systemd dependency. The official command maps both ports, persists data and logs to local volumes, and detaches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 9000:9000 &lt;span class="nt"&gt;-p&lt;/span&gt; 9001:9001 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/logs:/logs rustfs/rustfs:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[sourced from RustFS GitHub README, NOT EXECUTED IN CI]&lt;/p&gt;

&lt;p&gt;Do not drop the &lt;code&gt;-p&lt;/code&gt; or &lt;code&gt;-v&lt;/code&gt; flags. Without &lt;code&gt;-p 9000:9000&lt;/code&gt; the S3 API is unreachable from the host, and without the volume mounts a container restart loses everything under &lt;code&gt;/data&lt;/code&gt; and &lt;code&gt;/logs&lt;/code&gt;. The image tag &lt;code&gt;:latest&lt;/code&gt; tracks the newest build; pin a specific release like &lt;code&gt;1.0.0-rc.2&lt;/code&gt; if you need reproducibility. Podman users run the equivalent with &lt;code&gt;:Z,U&lt;/code&gt; on the volume mounts for SELinux. Either path lands you at the same console on 9001 and the same S3 endpoint on 9000.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you point an S3 client at your server?
&lt;/h2&gt;

&lt;p&gt;RustFS ships its own CLI, &lt;code&gt;rc&lt;/code&gt;, which handles both object operations and admin checks. After installing it, register an alias that stores the endpoint and keys, then verify the server is alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rc &lt;span class="nb"&gt;alias set local &lt;/span&gt;http://localhost:9000 &amp;lt;your-access-key&amp;gt; &amp;lt;your-secret-key&amp;gt;
rc ping &lt;span class="nb"&gt;local
&lt;/span&gt;rc ready &lt;span class="nb"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[sourced from RustFS docs (rc CLI), NOT EXECUTED IN CI]&lt;/p&gt;

&lt;p&gt;Create a bucket and push a file the same way any S3 client would:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rc bucket create &lt;span class="nb"&gt;local&lt;/span&gt;/my-bucket
rc object copy /path/to/hello.txt &lt;span class="nb"&gt;local&lt;/span&gt;/my-bucket/hello.txt
rc object list &lt;span class="nb"&gt;local&lt;/span&gt;/my-bucket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[sourced from RustFS docs (rc CLI), NOT EXECUTED IN CI]&lt;/p&gt;

&lt;p&gt;Because the API is S3-compatible, AWS SDKs, the &lt;code&gt;aws&lt;/code&gt; CLI with &lt;code&gt;--endpoint-url&lt;/code&gt;, and MinIO's &lt;code&gt;mc&lt;/code&gt; all work against port 9000 with the same keys. You are not locked into &lt;code&gt;rc&lt;/code&gt;; it is just the native client. Point your application at &lt;code&gt;http://&amp;lt;server-ip&amp;gt;:9000&lt;/code&gt; and the buckets behave like any other S3 endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What RustFS does today, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;RustFS today is a solid single-node S3-compatible server. The features marked available in the project's own status table include S3 core operations, upload/download, versioning, logging, event notifications, bucket replication, bitrot protection, single-node mode, multi-tenancy, Keystone auth, and the Swift API. If your workload fits one node, those are real and usable now.&lt;/p&gt;

&lt;p&gt;Be honest about the gaps. Distributed mode, lifecycle management, and RustFS KMS are still under testing, so multi-node erasure-coded clusters and automated tiering or expiry are not production promises yet. There is no Object Lock, no FUSE or POSIX mount, and no built-in erasure coding in the current release. RustFS does not ship a turnkey multi-region setup. For a single bare-metal node serving S3 to your apps, that is plenty. If you need cross-node redundancy or compliance retention today, plan around those limits rather than assuming they exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep a bare-metal server healthy?
&lt;/h2&gt;

&lt;p&gt;First, change the default credentials. The installer leaves placeholder keys in &lt;code&gt;/etc/default/rustfs&lt;/code&gt;; set your own access and secret keys there, then restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RUSTFS_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-access-key&amp;gt;
&lt;span class="nv"&gt;RUSTFS_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-secret-key&amp;gt;   &lt;span class="c"&gt;# e.g. output of: openssl rand -base64 24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart rustfs
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status rustfs &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[sourced from RustFS docs (Linux quick-start), NOT EXECUTED IN CI]&lt;/p&gt;

&lt;p&gt;Keep the service under systemd so it restarts on boot, and watch &lt;code&gt;systemctl status rustfs&lt;/code&gt; for the active (running) line. Because single-node mode has no redundancy, back up the data directory or replicate the bucket to a second site on a schedule you trust. Check disk space before it is gone, since a full volume stops writes. None of this is exotic, but it is yours to do. The console on 9001 shows bucket and object state when you want a visual check.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is RustFS free to self-host?
&lt;/h3&gt;

&lt;p&gt;Yes. RustFS is licensed under Apache 2.0, so you can run it on your own hardware at no license cost. You pay only for the server, disks, and your time to operate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the default RustFS credentials?
&lt;/h3&gt;

&lt;p&gt;The default access key and secret key are both &lt;code&gt;rustfsadmin&lt;/code&gt;. The installer warns you to replace them, and if you leave them unset the server falls back to those same defaults. Set &lt;code&gt;RUSTFS_ACCESS_KEY&lt;/code&gt; and &lt;code&gt;RUSTFS_SECRET_KEY&lt;/code&gt; in &lt;code&gt;/etc/default/rustfs&lt;/code&gt; and restart the service before exposing the port.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can RustFS replace MinIO on a single Linux server?
&lt;/h3&gt;

&lt;p&gt;For single-node S3-compatible needs, yes. Both speak the S3 API, so existing client code and SDKs work against either. RustFS is written in Rust, licensed Apache 2.0, and ships its own &lt;code&gt;rc&lt;/code&gt; CLI alongside the standard S3 interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does RustFS support multi-node distributed storage?
&lt;/h3&gt;

&lt;p&gt;Distributed mode is listed as under testing in the project's Feature &amp;amp; Status table, so it is not a production promise yet. Single-node mode is available and production-ready today. If you need cross-node erasure coding now, that gap should factor into your decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  What port does RustFS use?
&lt;/h3&gt;

&lt;p&gt;The S3 API listens on port 9000 and the web console on 9001. Both must be reachable: 9000 for client and application traffic, 9001 for the browser console.&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>s3</category>
      <category>objectstorage</category>
      <category>devops</category>
    </item>
    <item>
      <title>Build a Data Lake on S3-Compatible Storage</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Tue, 25 Aug 2026 11:50:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/build-a-data-lake-on-s3-compatible-storage-d5f</link>
      <guid>https://dev.to/ethan-carter/build-a-data-lake-on-s3-compatible-storage-d5f</guid>
      <description>&lt;p&gt;A data lake on S3-compatible storage is an object bucket holding open file formats (Parquet, ORC, JSON) partitioned by key, queried in place by engines like DuckDB, Spark, or Trino. You skip a proprietary warehouse, keep the raw data, and control the bill. The only hard requirement is a solid S3-compatible API.&lt;/p&gt;

&lt;p&gt;Every command below is copied verbatim from the official source cited beside it. This sandbox has no Docker daemon, so none of the commands were executed here; they are marked accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Stats&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fact&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS S3 has delivered strong read-after-write consistency for all objects since Dec 1, 2020&lt;/td&gt;
&lt;td&gt;AWS What's New (2020-12)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS limits each account to 10,000 buckets&lt;/td&gt;
&lt;td&gt;AWS S3 docs (BucketRestrictions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RustFS is Apache 2.0 licensed and S3-compatible&lt;/td&gt;
&lt;td&gt;RustFS GitHub README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DuckDB reads Parquet directly from &lt;code&gt;s3://&lt;/code&gt; through its httpfs extension&lt;/td&gt;
&lt;td&gt;DuckDB httpfs S3 API docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iceberg, Delta Lake, and Hudi all store Parquet data files plus metadata on object storage&lt;/td&gt;
&lt;td&gt;Project documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What is a data lake on S3-compatible storage?
&lt;/h2&gt;

&lt;p&gt;A data lake on S3-compatible storage is a bucket that stores raw and transformed data as files, not rows in a database. The files use open formats like Parquet, ORC, or JSON, and they sit under key prefixes that act like folders. An analytics engine reads those files over the S3 API and returns rows, but the data never has to move into a separate warehouse.&lt;/p&gt;

&lt;p&gt;The phrase S3-compatible matters because it widens your options. You can start on AWS S3, then run the same code against MinIO, Wasabi, Cloudflare R2, or a self-hosted RustFS cluster without rewriting your pipelines. The object store becomes a neutral substrate: cheap, durable, and reachable from every tool that speaks S3.&lt;/p&gt;

&lt;p&gt;In other words, a data lake is less a product and more a layout convention. Get the layout right and any engine can read it. Get it wrong and you rebuild queries every quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build your data lake on S3-compatible storage?
&lt;/h2&gt;

&lt;p&gt;You build a data lake on S3-compatible storage because the S3 API is the closest thing the data world has to a universal port. Every query engine, every backup tool, every ETL job already knows how to talk to it. That reach means you are never locked into one vendor's query semantics or one warehouse's pricing page.&lt;/p&gt;

&lt;p&gt;The cost angle is real but it is not automatic. Cloud S3 bills per GB-month plus per-request and egress charges, and egress is where lakes get expensive when you query from outside the region. Self-hosting flips that math: you pay for disks and bandwidth you already own, and you can co-locate compute so reads stay on the local network. The trade is operational. Someone runs the cluster, patches it, and owns the failures.&lt;/p&gt;

&lt;p&gt;For a small team, starting on a managed S3 endpoint and keeping the code portable is usually the pragmatic first move. You can move the data later without touching the queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you lay out a data lake in object storage?
&lt;/h2&gt;

&lt;p&gt;The layout is the part people skip and later regret. A data lake in object storage works best with Hive-style partitioning, where each key path encodes a dimension as a directory. A common shape is &lt;code&gt;events/dt=2026-08-14/region=us/part-001.parquet&lt;/code&gt;. Engines prune those prefixes, so a query for one day reads a tiny slice instead of the whole bucket.&lt;/p&gt;

&lt;p&gt;Keep file sizes sane. Object storage dislikes millions of tiny files: each is a separate GET with its own latency and listing overhead. Aim for files in the tens of megabytes, not kilobytes. If your producer emits small records, batch them before writing.&lt;/p&gt;

&lt;p&gt;Avoid deep nesting that no tool can prune, and keep a clear raw zone and a curated zone. Raw holds exactly what landed; curated holds the cleaned tables you actually query. The discipline pays off the first time you need to replay a day from raw.&lt;/p&gt;

&lt;p&gt;In other words, treat the key prefix as your primary index, because in object storage it is the only one you get for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which table formats sit on top of S3?
&lt;/h2&gt;

&lt;p&gt;On top of raw files, most teams add a table format so engines agree on schema, partitions, and snapshots. The three open options are Apache Iceberg, Delta Lake, and Apache Hudi. All three store Parquet data files plus JSON or AVRO metadata on object storage, and all three expose a table through the S3 API.&lt;/p&gt;

&lt;p&gt;Iceberg is the one I reach for first. Its metadata is designed for cloud object storage: snapshot isolation, hidden partitioning, and schema evolution that does not rewrite files. Delta Lake came out of the Spark world and stays tight with that ecosystem. Hudi targets incremental pipelines and record-level upserts.&lt;/p&gt;

&lt;p&gt;You can also skip a table format entirely and use Hive-style partitions with a catalog, which is plenty for append-only logs. The point is that the format is a layer on top of S3, not a replacement for it. Pick the engine-supported option and keep the files on S3 either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you query a data lake without moving it?
&lt;/h2&gt;

&lt;p&gt;This is the part that surprised me the first time: you can query Parquet straight off S3 with a single-node tool. DuckDB reads it through its httpfs extension. You register credentials with a secret, then point a query at an &lt;code&gt;s3://&lt;/code&gt; path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- DuckDB S3 secret (sourced from DuckDB httpfs S3 API docs, NOT EXECUTED IN CI)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="n"&gt;lake&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PROVIDER&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;KEY_ID&lt;/span&gt; &lt;span class="s1"&gt;'rustfsadmin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="s1"&gt;'rustfsadmin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;REGION&lt;/span&gt; &lt;span class="s1"&gt;'us-east-1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ENDPOINT&lt;/span&gt; &lt;span class="s1"&gt;'localhost:9000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;USE_SSL&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'s3://my-data-lake/raw/dt=2026-08-14/*.parquet'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ENDPOINT&lt;/code&gt; and &lt;code&gt;USE_SSL&lt;/code&gt; lines are the S3 secret parameters DuckDB documents for non-AWS hosts; for AWS you omit them. For heavier work, Spark or Trino scan the same prefixes across many nodes, and Iceberg or Delta catalogs give them schema and snapshot info.&lt;/p&gt;

&lt;p&gt;The win is that the data stays put. You are not loading it into a warehouse first; you read the lake where it lives. That single property is what keeps a data lake cheap compared to a copy-everywhere architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you stand up S3-compatible storage for a lake?
&lt;/h2&gt;

&lt;p&gt;Standing up your own S3-compatible endpoint is a handful of commands if you use RustFS. One Docker container gives you a bucket API that DuckDB, Spark, and the AWS CLI all understand. The official run command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sourced from RustFS GitHub README, NOT EXECUTED IN CI&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 9000:9000 &lt;span class="nt"&gt;-p&lt;/span&gt; 9001:9001 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/logs:/logs rustfs/rustfs:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default credentials are &lt;code&gt;rustfsadmin&lt;/code&gt; / &lt;code&gt;rustfsadmin&lt;/code&gt;. Point the MinIO client at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sourced from MinIO mc docs, NOT EXECUTED IN CI&lt;/span&gt;
mc &lt;span class="nb"&gt;alias set &lt;/span&gt;rustfs http://localhost:9000 rustfsadmin rustfsadmin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a bucket and load data with the AWS CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# mc mb sourced from MinIO mc docs; aws s3 cp recursive form per AWS CLI docs, NOT EXECUTED IN CI&lt;/span&gt;
mc mb rustfs/my-data-lake
aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; ./events/ s3://my-data-lake/raw/ &lt;span class="nt"&gt;--recursive&lt;/span&gt; &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; http://localhost:9000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a working lake: a bucket, partitioned data, and engines that can read it. Scale and durability are the parts you design next.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep a data lake safe?
&lt;/h2&gt;

&lt;p&gt;A data lake is only useful if you can recover it. Two S3 features earn their keep here, and both are worth turning on early.&lt;/p&gt;

&lt;p&gt;Versioning keeps every write as a new version instead of overwriting it. On AWS S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sourced from AWS CLI docs, NOT EXECUTED IN CI&lt;/span&gt;
aws s3api put-bucket-versioning &lt;span class="nt"&gt;--bucket&lt;/span&gt; amzn-s3-demo-bucket &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a self-hosted RustFS lake, versioning is an available feature, and bucket replication can mirror a bucket to a second site for disaster recovery. That replication path is available today.&lt;/p&gt;

&lt;p&gt;The catch is that versioning without lifecycle rules fills the bucket forever. On AWS you pair versioning with a lifecycle policy that expires old versions. On RustFS, lifecycle management is still under testing, so plan a manual or external cleanup job until that ships.&lt;/p&gt;

&lt;p&gt;In other words, protect the data first, then automate the cleanup. A lake you cannot restore is a liability, and a lake you never clean is a slowly growing bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks when you self-host a data lake?
&lt;/h2&gt;

&lt;p&gt;Self-hosting a data lake trades cloud convenience for control, and the rough edges show up in predictable places.&lt;/p&gt;

&lt;p&gt;Small files are the first one. Object stores charge per request and list slowly, so a lake built from millions of tiny objects crawls no matter which engine you use. Batch before you write.&lt;/p&gt;

&lt;p&gt;Consistency matters for pipelines that list then read. AWS S3 has been strongly consistent since December 2020, but self-hosted S3-compatible stores vary. Test list-after-write in your setup before you trust it for orchestration.&lt;/p&gt;

&lt;p&gt;Egress is the quiet tax. If compute runs in a different region or account from the bucket, every scan leaves the network and shows up on the bill. Co-locate compute with storage to avoid it.&lt;/p&gt;

&lt;p&gt;Finally, durability is your job now. A single node is a single point of failure. RustFS distributed mode is still under testing, so for production durability today you either replicate to a second node or back the bucket up offsite. Know which one you are doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need a warehouse like Snowflake to build a data lake?
&lt;/h3&gt;

&lt;p&gt;No. A data lake is just files on object storage plus a query engine. Snowflake, BigQuery, and friends are warehouses that copy data into their own format. You can query Parquet on S3 directly with DuckDB or Trino and skip the warehouse until you actually need one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can DuckDB really query S3 directly?
&lt;/h3&gt;

&lt;p&gt;Yes. DuckDB's httpfs extension reads Parquet over the S3 API using a &lt;code&gt;CREATE SECRET&lt;/code&gt; for credentials and endpoint. It is single-node, so it suits interactive analysis and moderate scans, not petabyte parallel jobs. For those, use Spark or Trino against the same files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is S3-compatible storage strongly consistent for a data lake?
&lt;/h3&gt;

&lt;p&gt;AWS S3 has been strongly consistent for all objects since December 2020, including reads after writes and list operations. Self-hosted S3-compatible stores do not all guarantee this, so verify list-after-write behavior before you build orchestration that depends on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which table format should I pick: Iceberg, Delta, or Hudi?
&lt;/h3&gt;

&lt;p&gt;Start with Apache Iceberg unless your stack is Spark-centric (then Delta) or you need record-level upserts from a streaming source (then Hudi). All three store Parquet plus metadata on S3 and are interoperable at the file level. The format is a layer on top of S3, not a lock-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does a self-hosted data lake cost versus AWS S3?
&lt;/h3&gt;

&lt;p&gt;AWS S3 Standard bills per GB-month plus per-request and egress charges, with the first 100 GB of internet egress free per month across services. Self-hosting replaces that with your own disk and bandwidth, which is cheaper at scale if compute is co-located. The real cost is operational: someone runs and patches the cluster. Price it as engineering time, not just hardware.&lt;/p&gt;

</description>
      <category>datalake</category>
      <category>s3</category>
      <category>objectstorage</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Object Storage vs File Storage: When to Use Which (2026)</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Sun, 23 Aug 2026 13:45:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/object-storage-vs-file-storage-when-to-use-which-2026-mc6</link>
      <guid>https://dev.to/ethan-carter/object-storage-vs-file-storage-when-to-use-which-2026-mc6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I still see engineers storing user-uploaded photos in &lt;code&gt;/var/www/uploads/&lt;/code&gt; on an ext4 volume and wondering why their server falls over at 10M files.&lt;/strong&gt; Meanwhile, the team next door threw the same photos into an S3 bucket and scaled to 100M files without breaking a sweat. The difference is the storage paradigm. Pick the wrong one and you feel it at scale.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I still mix the two up in conversation sometimes, so I keep a short checklist: random writes and file locks mean file storage; HTTP PUTs and billions of objects mean object storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; use file storage when you need POSIX semantics — in-place edits, sub-millisecond random I/O, file locking (databases, OS files, NFS shares). Use object storage when you need scale, an HTTP API and rich metadata (user uploads, data lakes, backups, ML datasets). Most mature stacks run both, side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Stats
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fact&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amazon S3 consistency&lt;/td&gt;
&lt;td&gt;Strong read-after-write for new objects, &lt;strong&gt;overwrites and LIST&lt;/strong&gt; — since Dec 1, 2020, at no extra cost&lt;/td&gt;
&lt;td&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2020/12/amazon-s3-now-delivers-strong-read-after-write-consistency-automatically-for-all-applications/" rel="noopener noreferrer"&gt;AWS What's New&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s3fs-fuse random writes&lt;/td&gt;
&lt;td&gt;"random writes or appends to files require rewriting the entire object"&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/s3fs-fuse/s3fs-fuse" rel="noopener noreferrer"&gt;s3fs-fuse README → Limitations&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;goofys write support&lt;/td&gt;
&lt;td&gt;"only sequential writes supported"; no symlinks/hardlinks; cannot rename directories with &amp;gt;1000 children&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kahing/goofys" rel="noopener noreferrer"&gt;goofys README → Current Status&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mountpoint for Amazon S3&lt;/td&gt;
&lt;td&gt;Does "not implement all the features of a POSIX file system" — no directory renaming, no symlinks, no edits to existing files&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/awslabs/mountpoint-s3" rel="noopener noreferrer"&gt;awslabs/mountpoint-s3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RustFS license&lt;/td&gt;
&lt;td&gt;Apache 2.0 (no AGPL restrictions)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;rustfs/rustfs README&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RustFS FUSE mount&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not offered.&lt;/strong&gt; The README's Feature &amp;amp; Status table lists no FUSE / POSIX mount driver — use a third-party S3 FUSE client against its S3 endpoint&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;rustfs/rustfs README&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Is the Fundamental Difference Between Object and File Storage?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;File Storage&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Object Storage&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data unit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;File (named byte sequence)&lt;/td&gt;
&lt;td&gt;Object (data + metadata + key)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Organization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hierarchical (directories/subdirectories)&lt;/td&gt;
&lt;td&gt;Flat (key namespace; &lt;code&gt;/&lt;/code&gt; is cosmetic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Access method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;POSIX (open/read/write/seek/close)&lt;/td&gt;
&lt;td&gt;HTTP REST API (PUT/GET/DELETE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mutability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In-place (change bytes 100-200 without touching 1-99)&lt;/td&gt;
&lt;td&gt;Immutable (overwrite = new version/new object)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metadata&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fixed attributes (name, size, permissions, timestamps)&lt;/td&gt;
&lt;td&gt;Rich &amp;amp; extensible (custom key-value tags, content-type, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scaling limit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Millions of files (inode exhaustion, metadata perf)&lt;/td&gt;
&lt;td&gt;Billions+ of objects (distributed metadata)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NFS, SMB, POSIX local (ext4, xfs, zfs)&lt;/td&gt;
&lt;td&gt;S3 API (HTTP/HTTPS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consistency model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strong (reads see writes immediately)&lt;/td&gt;
&lt;td&gt;Strong read-after-write on AWS S3 since Dec 2020 — covers new objects, overwrites &lt;em&gt;and&lt;/em&gt; LIST; S3-compatible systems vary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typical latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sub-millisecond (local) to milliseconds (NFS)&lt;/td&gt;
&lt;td&gt;Milliseconds (network round-trip)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OS-level operations, databases, home dirs&lt;/td&gt;
&lt;td&gt;Unstructured data at scale, web/mobile apps, analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When Should You Use File Storage?
&lt;/h2&gt;

&lt;p&gt;File storage is the right choice when your application (or OS) needs &lt;strong&gt;POSIX semantics&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 1: Operating System Files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;hosts&lt;/span&gt;
/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;log&lt;/span&gt;/&lt;span class="n"&gt;syslog&lt;/span&gt;
/&lt;span class="n"&gt;home&lt;/span&gt;/&lt;span class="n"&gt;user&lt;/span&gt;/.&lt;span class="n"&gt;bashrc&lt;/span&gt;
/&lt;span class="n"&gt;tmp&lt;/span&gt;/&lt;span class="n"&gt;processing_12345&lt;/span&gt;.&lt;span class="n"&gt;tmp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your OS expects file storage. It uses &lt;code&gt;open()&lt;/code&gt;, &lt;code&gt;read()&lt;/code&gt;, &lt;code&gt;write()&lt;/code&gt;, &lt;code&gt;seek()&lt;/code&gt; — not HTTP PUT/GET. Don't fight this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 2: Databases
&lt;/h3&gt;

&lt;p&gt;PostgreSQL, MySQL, MongoDB, SQLite — they all expect block devices or file systems with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sub-millisecond random I/O&lt;/strong&gt; (index lookups, page reads)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-place mutation&lt;/strong&gt; (UPDATE SET field = value changes specific bytes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong consistency&lt;/strong&gt; (ACID transactions depend on ordered fsync)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File locking&lt;/strong&gt; (&lt;code&gt;.lock&lt;/code&gt; files, advisory locks)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Object storage has millisecond-level latency and no in-place mutation. Databases on S3 perform terribly (with niche exceptions like Iceberg/Delta lakehouse patterns).&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 3: Network File Sharing (NFS/SMB)
&lt;/h3&gt;

&lt;p&gt;When multiple users/servers need shared access to the same files with familiar tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design teams sharing Figma/Adobe files via SMB mount&lt;/li&gt;
&lt;li&gt;Build servers sharing source code via NFS&lt;/li&gt;
&lt;li&gt;Home directories in enterprise environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These use cases need &lt;strong&gt;file-level permissions, directory browsing, and application transparency&lt;/strong&gt; — all strengths of file storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 4: Small-Scale Applications (&amp;lt; 100K files, &amp;lt; 1TB)
&lt;/h3&gt;

&lt;p&gt;For small datasets, file storage is simpler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No API to learn&lt;/li&gt;
&lt;li&gt;Familiar tools (&lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Easy backups (tar, rsync)&lt;/li&gt;
&lt;li&gt;Local access = fastest possible&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Should You Use Object Storage?
&lt;/h2&gt;

&lt;p&gt;Object storage is the right choice when you need &lt;strong&gt;scale, simplicity of API, and rich metadata:&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 1: User-Generated Content
&lt;/h3&gt;

&lt;p&gt;Photos, videos, documents, uploads — the canonical object storage workload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# User uploads photo
&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user-photos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/photo-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;image_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/jpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uploader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;camera&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iphone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scale from 1K to 100M objects without changing code. I have watched teams try to stretch a filesystem to that size; inode exhaustion is not a fun afternoon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 2: Data Lake / Analytics
&lt;/h3&gt;

&lt;p&gt;Parquet/Avro/CSV files for Spark, Trino, DuckDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3://data-lake/bronze/events/year=2026/month=07/day=24/event-*.parquet
s3://data-lake/gold/daily_active_users.parquet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flat namespace, massive scale, accessed by query engines that speak S3 natively. This is where object storage dominates in 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 3: Backup &amp;amp; Archive
&lt;/h3&gt;

&lt;p&gt;Database dumps, VM snapshots, compliance records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Immutable&lt;/strong&gt; (versioning = accidental deletion protection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiered&lt;/strong&gt; (lifecycle policies move old data to cheap storage automatically)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replicated&lt;/strong&gt; (cross-region for DR)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliant&lt;/strong&gt; (Object Lock for WORM retention)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;File storage can do backups too, but at scale, object storage's tiering and replication features save significant cost and operational effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 4: Static Website / CDN Origin
&lt;/h3&gt;

&lt;p&gt;S3 + CloudFront (or Cloudflare) is the standard pattern for serving static web content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Objects = web assets (HTML, CSS, JS, images)&lt;/li&gt;
&lt;li&gt;Global CDN = fast delivery everywhere&lt;/li&gt;
&lt;li&gt;HTTPS + custom domain = zero-infrastructure frontend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Serving a global static site from an NFS mount is not something I'd want to run on-call for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 5: Machine Learning &amp;amp; AI
&lt;/h3&gt;

&lt;p&gt;Training data, model checkpoints, inference outputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checkpointing:&lt;/strong&gt; Write model state as object → resume from any saved point&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset versioning:&lt;/strong&gt; Each dataset version = immutable object (reproducible training)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature stores:&lt;/strong&gt; Parquet objects queried by ML frameworks via S3 API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ML workloads at scale (terabytes of training data) are almost always object-storage-backed in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can You Mount S3 as a Filesystem?
&lt;/h2&gt;

&lt;p&gt;What if you want S3's scale but need file-system semantics? I have been asked this in almost every object-storage migration. The honest answer is: you can, but the mount layer will lie to you in small ways. Here is what each project officially documents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Officially documented limits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/s3fs-fuse/s3fs-fuse" rel="noopener noreferrer"&gt;s3fs-fuse&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;td&gt;Mounts an S3 bucket via FUSE on Linux/macOS/FreeBSD; preserves the native object format so &lt;code&gt;aws s3&lt;/code&gt; still works&lt;/td&gt;
&lt;td&gt;"random writes or appends to files require rewriting the entire object"; "no atomic renames of files or directories"; "no hard links"; "no coordination between multiple clients mounting the same bucket"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kahing/goofys" rel="noopener noreferrer"&gt;goofys&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;A "Filey System" that "strives for performance first and POSIX second"; close-to-open consistency, no on-disk cache&lt;/td&gt;
&lt;td&gt;"only sequential writes supported"; "does not support symlink or hardlink"; "cannot &lt;code&gt;rename&lt;/code&gt; directories with more than 1000 children"; "&lt;code&gt;fsync&lt;/code&gt; is ignored" — and the last commit was &lt;strong&gt;June 2023&lt;/strong&gt;, so treat it as low-maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/awslabs/mountpoint-s3" rel="noopener noreferrer"&gt;Mountpoint for Amazon S3&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;AWS's own GA file client, tuned for high read throughput and sequential writes of new objects&lt;/td&gt;
&lt;td&gt;AWS states it is "probably not the right fit" for apps that use "directory renaming or symlinks" or "make edits to existing files (don't work on your Git repository or run &lt;code&gt;vim&lt;/code&gt; in Mountpoint)"; support for non-AWS S3-compatible stores is limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://rclone.org/commands/rclone_mount/" rel="noopener noreferrer"&gt;rclone mount&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Mounts any of rclone's 70+ backends, including any S3-compatible endpoint&lt;/td&gt;
&lt;td&gt;rclone's own docs warn the file system is not fully POSIX-compliant; behaviour depends on VFS cache mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Own it first:&lt;/strong&gt; RustFS does &lt;strong&gt;not&lt;/strong&gt; ship a FUSE driver. Its README Feature &amp;amp; Status table covers S3 core, versioning, bucket replication, event notifications, bitrot protection, Swift/Keystone and Helm charts — no POSIX mount. If you want a mount, point one of the clients above at RustFS's S3 endpoint like you would at any other S3 service. Anyone telling you a "native RustFS mount" exists is reading a spec sheet that doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance reality:&lt;/strong&gt; the translation layer is POSIX → HTTP, so each metadata operation becomes a network round trip. s3fs-fuse names this explicitly: "metadata operations such as listing directories have poor performance due to network latency." That's fine for bulk work — &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;tar&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, feeding a training job. It is &lt;strong&gt;not&lt;/strong&gt; fine for databases, build systems or anything doing high-IOPS random writes, because those turn into whole-object rewrites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Should You Pick? A Decision Flowchart
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do you need sub-millisecond random I/O?
├─ YES → File Storage (database, OS files)
│         (or block storage)
│
└─ NO → Do you need POSIX semantics (ls, chmod, flock)?
    ├─ YES → File Storage (NFS/SMB shares, source code)
    │
    └─ NO → Will you exceed 1M files/objects?
       ├─ YES → Object Storage (S3/S3-compatible)
       │         (photos, data lake, backups, ML)
       │
       └─ NO → Either works; pick the simpler tool
                 for your team's skill set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;File storage = hierarchical, mutable, POSIX, scales to millions.&lt;/strong&gt; Use it for OS files, databases, NFS shares, small datasets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object storage = flat, immutable, HTTP API, scales to billions.&lt;/strong&gt; Use it for user content, data lakes, backups/archives, static sites, ML data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The crossover point is usually scale.&lt;/strong&gt; Under 100K files/1TB: file storage is simpler. Over that: object storage wins on operational cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databases always want file/block storage&lt;/strong&gt; (not object). ML/analytics always want object storage (not file).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 FUSE clients (s3fs-fuse, goofys, AWS Mountpoint, &lt;code&gt;rclone mount&lt;/code&gt;)&lt;/strong&gt; give you file-system access to S3 data — every one of them documents real POSIX gaps (no atomic renames, sequential-writes-only, no edits to existing files). Fine for bulk I/O, wrong for databases. RustFS itself ships no FUSE driver; use one of these against its S3 endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can use both.&lt;/strong&gt; Most mature infrastructures I have worked with run file storage and object storage side-by-side. The cleanest architecture is usually "file storage for the OS and databases, object storage for everything else" rather than forcing one paradigm to cover both.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Need S3-compatible object storage you can run yourself? &lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt; is Apache 2.0 licensed (no AGPL strings) and its README lists S3 core, versioning, bucket replication, event notifications, bitrot protection, multi-tenancy and Helm charts as Available; Lifecycle Management, Distributed Mode and KMS are still marked Under Testing — so plan accordingly. Try it in one command:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 9000:9000 &lt;span class="nt"&gt;-p&lt;/span&gt; 9001:9001 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/logs:/logs rustfs/rustfs:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;[sourced verbatim from the &lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;RustFS GitHub README&lt;/a&gt; — NOT EXECUTED IN CI]. Console on port 9001, default credentials &lt;code&gt;rustfsadmin&lt;/code&gt; / &lt;code&gt;rustfsadmin&lt;/code&gt; — change them before you expose anything. Binaries and the &lt;code&gt;rc&lt;/code&gt; CLI: &lt;a href="https://rustfs.com/download" rel="noopener noreferrer"&gt;rustfs.com/download&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I replace NFS with S3/object storage?
&lt;/h3&gt;

&lt;p&gt;Sometimes. For plain file sharing I usually skip the FUSE shim and serve objects through a web UI or pre-signed URLs — one less POSIX lie to debug. If you really need a mount, read the limits first: s3fs-fuse has "no atomic renames of files or directories" and "no coordination between multiple clients mounting the same bucket"; goofys supports "only sequential writes"; Mountpoint refuses edits to existing files. Compilers, build systems, anything calling &lt;code&gt;flock()&lt;/code&gt; — those stay on real file storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which is faster, object storage or file storage?
&lt;/h3&gt;

&lt;p&gt;It depends where the reader is sitting. A local NVMe filesystem wins for a single machine; object storage wins when you need a CDN in front of it. The question I ask is not "which is faster" but "which is fast enough at this distance". Databases need the local path. A photo served worldwide needs the CDN path. I ignore quoted millisecond figures unless they come with the test setup attached.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can databases run on object storage?
&lt;/h3&gt;

&lt;p&gt;Traditional OLTP databases — PostgreSQL, MySQL — no, not as their primary data directory. They need in-place mutation, ordered &lt;code&gt;fsync&lt;/code&gt; and sub-millisecond random reads, none of which object storage provides. What does work, and works extremely well, is the lakehouse pattern: query engines such as DuckDB (via &lt;code&gt;httpfs&lt;/code&gt;), Trino, Spark and ClickHouse (S3 table engine) read Parquet/ORC directly out of S3, and table formats like Apache Iceberg and Delta Lake add ACID semantics on top of immutable objects. Object storage is also the universal &lt;em&gt;backup target&lt;/em&gt; for databases. So the accurate statement is: analytics on object storage, yes; transactional storage engine on object storage, no.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I migrate from file storage to object storage?
&lt;/h3&gt;

&lt;p&gt;Gradually, and by workload rather than by directory. A path that works: (1) point all &lt;em&gt;new&lt;/em&gt; workloads at S3 from day one; (2) move user-generated content first — uploads are the natural fit; (3) move analytics data next, as Parquet in a bucket queried by Spark/Trino/DuckDB; (4) leave the legacy file server on NFS/SMB and mirror it to object storage for DR and archive; (5) never move OS files. &lt;code&gt;rclone sync&lt;/code&gt; handles filesystem-to-S3 copies against any S3-compatible endpoint, and &lt;code&gt;aws s3 sync&lt;/code&gt; works for AWS. Budget for a coexistence period — both paradigms running side by side is the normal end state, not a failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does RustFS provide a POSIX or FUSE mount?
&lt;/h3&gt;

&lt;p&gt;No. As of the check date on this article, the RustFS GitHub README's Feature &amp;amp; Status table lists S3 Core Features, Upload/Download, Versioning, Logging, Event Notifications, K8s Helm Charts, Keystone Auth, Swift API, Bitrot Protection, Single Node Mode, Bucket Replication and Multi-Tenancy as Available, with Lifecycle Management, Distributed Mode and RustFS KMS marked Under Testing. There is no FUSE driver, no &lt;code&gt;rustfs mount&lt;/code&gt; command and no POSIX mount feature anywhere in the README or on docs.rustfs.com. If you need a mount, run s3fs-fuse, goofys or &lt;code&gt;rclone mount&lt;/code&gt; against the RustFS S3 endpoint on port 9000 — exactly as you would against any other S3-compatible service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;All claims above were checked against primary sources on &lt;strong&gt;2026-08-07&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Amazon S3 strong read-after-write consistency (new objects, overwrites, LIST; Dec 1, 2020) — &lt;a href="https://aws.amazon.com/about-aws/whats-new/2020/12/amazon-s3-now-delivers-strong-read-after-write-consistency-automatically-for-all-applications/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2020/12/amazon-s3-now-delivers-strong-read-after-write-consistency-automatically-for-all-applications/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;s3fs-fuse "Limitations" section (random writes rewrite the whole object, no atomic renames, no hard links, no multi-client coordination) — &lt;a href="https://github.com/s3fs-fuse/s3fs-fuse" rel="noopener noreferrer"&gt;https://github.com/s3fs-fuse/s3fs-fuse&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;goofys "Current Status" non-POSIX behaviours (sequential writes only, no symlink/hardlink, 1000-child rename cap, &lt;code&gt;fsync&lt;/code&gt; ignored); last commit June 2023 — &lt;a href="https://github.com/kahing/goofys" rel="noopener noreferrer"&gt;https://github.com/kahing/goofys&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Mountpoint for Amazon S3 POSIX caveats (no directory renaming, no symlinks, no edits to existing files) — &lt;a href="https://github.com/awslabs/mountpoint-s3" rel="noopener noreferrer"&gt;https://github.com/awslabs/mountpoint-s3&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rclone mount&lt;/code&gt; documentation — &lt;a href="https://rclone.org/commands/rclone_mount/" rel="noopener noreferrer"&gt;https://rclone.org/commands/rclone_mount/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RustFS license, Feature &amp;amp; Status table, quickstart command and default credentials — &lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;https://github.com/rustfs/rustfs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RustFS installation documentation (no FUSE/POSIX mount path listed) — &lt;a href="https://docs.rustfs.com/" rel="noopener noreferrer"&gt;https://docs.rustfs.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>objectstorage</category>
      <category>filestorage</category>
      <category>s3</category>
      <category>nfs</category>
    </item>
    <item>
      <title>Using S3 as Your Disaster Recovery Target: A Practical Guide</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:40:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/using-s3-as-your-disaster-recovery-target-a-practical-guide-3f0b</link>
      <guid>https://dev.to/ethan-carter/using-s3-as-your-disaster-recovery-target-a-practical-guide-3f0b</guid>
      <description>&lt;h1&gt;
  
  
  Using S3 as Your Disaster Recovery Target: A Practical Guide
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;S3 works as a disaster-recovery target because your data is already there, it is redundant by design, and cross-region replication is a configuration change rather than a new pipeline.&lt;/strong&gt; The case for bothering is in the outage economics: Uptime Institute's 2022 Outage Analysis found that &lt;strong&gt;over 60% of failures now cost at least $100,000&lt;/strong&gt; — up from 39% in 2019 — and the share costing more than $1 million climbed from 11% to 15%. Yet when I audit DR plans, S3 is consistently the most underused asset in the room.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Key Stats
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Figure&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Failures costing ≥ $100,000&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;60%+&lt;/strong&gt; (was 39% in 2019)&lt;/td&gt;
&lt;td&gt;Uptime Institute, 2022 Outage Analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failures costing ≥ $1M&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;15%&lt;/strong&gt; (was 11%)&lt;/td&gt;
&lt;td&gt;Uptime Institute, 2022 Outage Analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orgs hit by a serious/severe outage in 3 years&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 in 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Uptime Institute, 2022 Outage Analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public outages lasting &amp;gt; 24 hours (2021)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~30%&lt;/strong&gt; (was 8% in 2017)&lt;/td&gt;
&lt;td&gt;Uptime Institute, 2022 Outage Analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Major outages caused by human error&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~40%&lt;/strong&gt; of orgs; 85% trace to procedure failures&lt;/td&gt;
&lt;td&gt;Uptime Institute, 2022 Outage Analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS S3 designed durability&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;99.999999999%&lt;/strong&gt; (11 nines)&lt;/td&gt;
&lt;td&gt;AWS S3 official documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row on human error is the one worth sitting with: the failure mode is rarely "the storage broke." It is that nobody had run the restore.&lt;/p&gt;

&lt;p&gt;I have audited enough DR plans to notice a pattern: the teams that survive outages are not the ones with the most elegant architecture; they are the ones that actually tested a restore. S3-compatible storage is just the cheapest place to start that habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does S3 work well as a DR target?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It's Already There
&lt;/h3&gt;

&lt;p&gt;Most applications I look at already write something to S3, even if the team does not think of it as primary storage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User-uploaded files (photos, documents, media)&lt;/li&gt;
&lt;li&gt;Log files and audit trails&lt;/li&gt;
&lt;li&gt;Database backups (mysqldump, pg_dump, volume snapshots)&lt;/li&gt;
&lt;li&gt;ML model artifacts and datasets&lt;/li&gt;
&lt;li&gt;Configuration backups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If this data is already in S3, making it DR-capable is mostly a configuration change, not a new pipeline.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  It's Durable by Design
&lt;/h3&gt;

&lt;p&gt;S3 (and serious S3-compatible implementations) stores data redundantly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Stated Durability&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS S3&lt;/td&gt;
&lt;td&gt;99.999999999% (11 nines), &lt;em&gt;designed for&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Redundant storage across multiple devices in ≥3 Availability Zones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backblaze B2&lt;/td&gt;
&lt;td&gt;99.999999999% (11 nines), annual&lt;/td&gt;
&lt;td&gt;Reed-Solomon erasure coding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;No published nines figure&lt;/td&gt;
&lt;td&gt;README lists &lt;strong&gt;Bitrot Protection&lt;/strong&gt; and &lt;strong&gt;Bucket Replication&lt;/strong&gt; as ✅ Available; &lt;strong&gt;Distributed Mode&lt;/strong&gt; is 🚧 Under Testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MinIO&lt;/td&gt;
&lt;td&gt;No published nines figure&lt;/td&gt;
&lt;td&gt;Erasure coding or replication, configurable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I am deliberately not inventing a durability number for RustFS or MinIO. Neither project publishes an audited nines figure, and a self-hosted cluster's real durability depends on your disk count, erasure set width and failure domains — not on the vendor's marketing page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-disk failure = zero data loss.&lt;/strong&gt; Single-node failure (in clustered setups) = zero data loss. This is better than most on-premises databases achieve out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's Cheap (Relative to Alternatives)
&lt;/h3&gt;

&lt;p&gt;DR is insurance — nobody wants to overpay for it. Concretely: AWS S3 Standard list price is &lt;strong&gt;$0.023/GB-month&lt;/strong&gt;, so parking 10 TB of DR backups costs about &lt;strong&gt;$236/month&lt;/strong&gt;. A warm-standby database instance sized for the same workload, plus its attached block storage, generally lands in four figures a month. I am not going to quote you a tidy multiple, because the ratio swings hard with instance class, retention and egress — run the numbers for your own shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which DR architecture pattern should you use?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern 1: Backup-to-S3 (What I recommend first)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Production DB ──[daily dump]──▶ S3 Bucket (primary region)
                                    │
                              Cross-region replicate
                                    │
                                    ▶
                              S3 Bucket (DR region)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Nightly database dump → compressed file → PUT to S3&lt;/li&gt;
&lt;li&gt;S3 Cross-Region Replication (CRR) copies to DR region&lt;/li&gt;
&lt;li&gt;If primary region fails: spin up DB in DR region → restore from S3&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;RTO (Recovery Time Objective):&lt;/strong&gt; 1-4 hours (depends on DB size + restore speed)&lt;br&gt;
&lt;strong&gt;RPO (Recovery Point Objective):&lt;/strong&gt; Up to 24 hours (backup frequency)&lt;/p&gt;

&lt;p&gt;This is not exciting architecture, but it is the pattern that saves most teams. Get backups into a second region before you worry about streaming replication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; &lt;code&gt;aws s3 sync&lt;/code&gt;, rclone, database-native S3 backup tools (pg_backrest, mysqldump + pipe)&lt;/p&gt;
&lt;h3&gt;
  
  
  Pattern 2: Continuous Log Shipping (Better RPO)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Production DB WAL/Binlog ──[stream]──▶ S3 (primary)
                                         │
                                   CRR / Custom forwarder
                                         │
                                         ▶
                                   S3 (DR region)
                                         │
                                   [Continuous restore]
                                         ▶
                                   Standby DB (DR region)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write-Ahead Logs (PostgreSQL) or Binlogs (MySQL) stream to S3 continuously&lt;/li&gt;
&lt;li&gt;Standby DB in DR region applies logs in near-real-time&lt;/li&gt;
&lt;li&gt;If primary fails: promote standby (seconds to minutes of RPO)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;RTO:&lt;/strong&gt; Minutes (standby is already running, just needs promotion)&lt;br&gt;
&lt;strong&gt;RPO:&lt;/strong&gt; Seconds to minutes (depends on log shipping lag)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; WAL-G (PostgreSQL), pgBackRest, MySQL binlog-to-s3 tools, Debezium (CDC)&lt;/p&gt;
&lt;h3&gt;
  
  
  Pattern 3: Active-Active with S3 as Source of Truth (Avoid unless you need it)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Region A App ◄──── S3 (shared, replicated) ───► Region B App
                    │
              [Both regions read/write to same S3]
              [App-level conflict resolution required]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Both regions' applications read/write to the same S3 bucket(s)&lt;/li&gt;
&lt;li&gt;Conflict resolution is your application's job — object storage does not merge concurrent writes for you&lt;/li&gt;
&lt;li&gt;If Region A fails: Region B continues serving with zero RPO&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;RTO:&lt;/strong&gt; Zero (automatic)&lt;br&gt;
&lt;strong&gt;RPO:&lt;/strong&gt; Zero (both regions always current)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complexity:&lt;/strong&gt; High. I have seen this pattern look simple on a whiteboard and turn into weeks of conflict-resolution bugs in production. Use it only when zero RPO is a hard business requirement, not because it sounds modern.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementing Pattern 1: The Minimal Viable DR Plan
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Identify Critical Data
&lt;/h3&gt;

&lt;p&gt;Not all data needs DR protection. Classify yours:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Data Type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;DR Requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T0 (Critical)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User data, financial transactions&lt;/td&gt;
&lt;td&gt;User uploads, payment records&lt;/td&gt;
&lt;td&gt;RPO &amp;lt; 1hr, RTO &amp;lt; 1hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T1 (Important)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Business operational data&lt;/td&gt;
&lt;td&gt;Logs, configs, ML models&lt;/td&gt;
&lt;td&gt;RPO &amp;lt; 24hr, RTO &amp;lt; 4hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T2 (Useful)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Analytics, historical&lt;/td&gt;
&lt;td&gt;Aggregated data, old backups&lt;/td&gt;
&lt;td&gt;RPO &amp;lt; 7 days, RTO &amp;lt; 24hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T3 (Disposable)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cache, temp files&lt;/td&gt;
&lt;td&gt;Session stores, build artifacts&lt;/td&gt;
&lt;td&gt;Best effort / none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Focus DR effort on T0 and T1. T2 and T3 are nice-to-have.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Set Up Automated Backups to S3
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# dr-backup.sh — Daily backup to S3&lt;/span&gt;
&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dr-backups-primary"&lt;/span&gt;

&lt;span class="c"&gt;# PostgreSQL backup&lt;/span&gt;
pg_dump &lt;span class="nt"&gt;-Fc&lt;/span&gt; production_db | &lt;span class="nb"&gt;gzip&lt;/span&gt; | aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; - &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/postgres/&lt;/span&gt;&lt;span class="nv"&gt;$DATE&lt;/span&gt;&lt;span class="s2"&gt;/db.dump.gz"&lt;/span&gt;

&lt;span class="c"&gt;# Application data sync&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;sync&lt;/span&gt; /data/app-uploads &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/uploads/&lt;/span&gt;&lt;span class="nv"&gt;$DATE&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="nt"&gt;--delete&lt;/span&gt;

&lt;span class="c"&gt;# Config backup&lt;/span&gt;
&lt;span class="nb"&gt;tar &lt;/span&gt;czf - /etc/myapp/config | aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; - &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/config/&lt;/span&gt;&lt;span class="nv"&gt;$DATE&lt;/span&gt;&lt;span class="s2"&gt;/config.tar.gz"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] DR backup complete"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/log/dr-backup.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Schedule via cron (every 6 hours for T0, daily for T1):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 &lt;span class="k"&gt;*&lt;/span&gt;/6 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /opt/scripts/dr-backup.sh    &lt;span class="c"&gt;# T0: Every 6 hours&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /opt/scripts/dr-backup-full.sh &lt;span class="c"&gt;# T1: Daily at 2 AM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Configure Cross-Region Replication
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;On AWS.&lt;/strong&gt; Replication requires versioning on &lt;em&gt;both&lt;/em&gt; buckets, and &lt;code&gt;Role&lt;/code&gt; is a &lt;strong&gt;required&lt;/strong&gt; top-level field in the replication configuration — a lot of copy-pasted snippets omit it and fail with &lt;code&gt;InvalidRequest&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# [sourced from https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-replication.html, NOT EXECUTED IN CI]&lt;/span&gt;

&lt;span class="c"&gt;# Prerequisite: versioning on BOTH source and destination&lt;/span&gt;
aws s3api put-bucket-versioning &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; dr-backups-primary &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled

aws s3api put-bucket-versioning &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; dr-backups-dr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled

&lt;span class="c"&gt;# CRR rule ("Role" is REQUIRED — the IAM role S3 assumes to replicate)&lt;/span&gt;
aws s3api put-bucket-replication &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; dr-backups-primary &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--replication-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Role": "arn:aws:iam::123456789012:role/s3-replication-role",
    "Rules": [{
      "Status": "Enabled",
      "Priority": 1,
      "DeleteMarkerReplication": { "Status": "Enabled" },
      "Filter": { "Prefix": "" },
      "Destination": {
        "Bucket": "arn:aws:s3:::dr-backups-dr",
        "StorageClass": "STANDARD"
      }
    }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On self-hosted S3.&lt;/strong&gt; RustFS lists &lt;strong&gt;Bucket Replication&lt;/strong&gt; as ✅ Available in its README Feature &amp;amp; Status table. If you want a scheduled, engine-agnostic copy between two independent clusters — which is what most self-hosted DR setups actually run — &lt;code&gt;rclone&lt;/code&gt; is the pragmatic tool. Define two named S3 remotes, then sync:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.config/rclone/rclone.conf
# [sourced from https://rclone.org/s3/, NOT EXECUTED IN CI]
&lt;/span&gt;&lt;span class="nn"&gt;[primary]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
&lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Other&lt;/span&gt;
&lt;span class="py"&gt;access_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;rustfsadmin&lt;/span&gt;
&lt;span class="py"&gt;secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;rustfsadmin&lt;/span&gt;
&lt;span class="py"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;http://primary-site:9000&lt;/span&gt;

&lt;span class="nn"&gt;[dr]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
&lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Other&lt;/span&gt;
&lt;span class="py"&gt;access_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;rustfsadmin&lt;/span&gt;
&lt;span class="py"&gt;secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;rustfsadmin&lt;/span&gt;
&lt;span class="py"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;http://dr-site:9000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# [sourced from https://rclone.org/commands/rclone_sync/, NOT EXECUTED IN CI]&lt;/span&gt;
rclone &lt;span class="nb"&gt;sync &lt;/span&gt;primary:dr-backups-primary dr:dr-backups-dr &lt;span class="nt"&gt;--progress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then put that behind cron every 30 minutes. Note &lt;code&gt;rclone sync&lt;/code&gt; makes the destination &lt;em&gt;match&lt;/em&gt; the source — it deletes objects at the destination that no longer exist at the source. If you want DR to survive an accidental mass-delete on the primary, use &lt;code&gt;rclone copy&lt;/code&gt; instead, or enable versioning and Object Lock on the DR bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Document &amp;amp; Test the Restore Procedure
&lt;/h3&gt;

&lt;p&gt;Your DR plan is only useful if the person on call can execute it without calling you. I have been that 3 a.m. call; write the runbook for someone who has not seen it before:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DR Runbook: Primary Region Failure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger conditions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary region unreachable for &amp;gt; 15 min&lt;/li&gt;
&lt;li&gt;Major data corruption detected&lt;/li&gt;
&lt;li&gt;Executive decision to failover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole procedure runs about 45–90 minutes. Each step is a separate command block you can paste as-is.&lt;/p&gt;

&lt;p&gt;1.&lt;strong&gt;Verify DR bucket integrity&lt;/strong&gt; (5 min)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://dr-backups-dr/ &lt;span class="nt"&gt;--recursive&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
   &lt;span class="c"&gt;# Compare count to expected object count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2.&lt;strong&gt;Promote DR database&lt;/strong&gt; (15-45 min)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;# Start the instance in the DR region, then stream the dump straight out of S3.&lt;/span&gt;
   &lt;span class="c"&gt;# NOTE: gunzip cannot read an s3:// URL — you must pipe through `aws s3 cp ... -`.&lt;/span&gt;
   &lt;span class="nv"&gt;LATEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://dr-backups-dr/postgres/ | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
   aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"s3://dr-backups-dr/postgres/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LATEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;db.dump.gz"&lt;/span&gt; - &lt;span class="se"&gt;\&lt;/span&gt;
     | &lt;span class="nb"&gt;gunzip&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     | pg_restore &lt;span class="nt"&gt;-d&lt;/span&gt; production_db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3.&lt;strong&gt;Update DNS&lt;/strong&gt; (2-30 min, depends on TTL)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;# Route53: Change A record to DR instance IP&lt;/span&gt;
   &lt;span class="c"&gt;# Wait for DNS propagation (monitor with dig + health checks)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4.&lt;strong&gt;Verify application health&lt;/strong&gt; (10 min)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl https://myapp.com/healthcheck
   &lt;span class="c"&gt;# Expect: {"status":"ok","region":"dr"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5.&lt;strong&gt;Communicate&lt;/strong&gt; (ongoing)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Status page update&lt;/li&gt;
&lt;li&gt;Internal Slack alert&lt;/li&gt;
&lt;li&gt;Customer notification (if SLA impacted)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test this runbook quarterly.&lt;/strong&gt; An untested runbook is just a theory, and outages are bad at following theories.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes wrong with S3 DR?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Replication lag will surprise you
&lt;/h3&gt;

&lt;p&gt;CRR is asynchronous. An object written at T0 might not appear in the DR bucket until T0 + 15 minutes (or longer under load). &lt;strong&gt;Your latest backup might not yet be in DR when you need it.&lt;/strong&gt; This is the failure mode I check first in any DR drill.&lt;/p&gt;

&lt;p&gt;What helps: check replication lag metrics before declaring disaster, and keep a force-sync procedure for critical objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption keys live in one region
&lt;/h3&gt;

&lt;p&gt;If your primary region uses AWS KMS-managed keys and that region is down... &lt;strong&gt;you can't decrypt backups in the DR region&lt;/strong&gt; unless you've planned for cross-region KMS access. I have seen a perfectly good DR bucket become useless because the key was still in the failed region.&lt;/p&gt;

&lt;p&gt;What helps: use client-side encryption (you hold the key) or ensure the KMS key is accessible from the DR region (multi-Region KMS key).&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 is not a database DR mechanism
&lt;/h3&gt;

&lt;p&gt;S3 is great for DR &lt;em&gt;of things stored in S3&lt;/em&gt;. It is not a replacement for database replication. Your PostgreSQL primary still needs streaming replication, logical replication, or Patroni — S3 is the safety net, not the primary mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;S3 is probably already your biggest DR asset&lt;/strong&gt; — most critical data lands there eventually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three patterns:&lt;/strong&gt; Backup-to-S3 (simplest, RPO=hours), Log shipping (better, RPO=minutes), Active-Active (best, complex).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with Pattern 1&lt;/strong&gt; (automated backups + CRR) — it's a 1-day setup that covers 80% of DR scenarios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test restores quarterly.&lt;/strong&gt; An untested DR plan is a false sense of security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 complements — doesn't replace — database replication.&lt;/strong&gt; Use both.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Building a DR target on self-hosted S3? &lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt; is Apache 2.0 licensed and its &lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;README Feature &amp;amp; Status table&lt;/a&gt; lists **Bucket Replication&lt;/em&gt;&lt;em&gt;, **Versioning&lt;/em&gt;&lt;em&gt;, **Bitrot Protection&lt;/em&gt;* and &lt;strong&gt;Event Notifications&lt;/strong&gt; as ✅ Available — the four primitives a DR pipeline actually leans on. Being straight with you: &lt;strong&gt;Distributed Mode is still 🚧 Under Testing&lt;/strong&gt;, so validate your multi-node topology yourself before betting a production DR plan on it. &lt;a href="https://rustfs.com/download" rel="noopener noreferrer"&gt;Download here&lt;/a&gt;.*&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is S3 enough for disaster recovery, or do I need a database replica?
&lt;/h3&gt;

&lt;p&gt;You need both. S3 handles the non-transactional layer — files, logs, backups, artifacts. For PostgreSQL or MySQL you still need streaming or logical replication if you want RPO measured in seconds. I treat S3 DR as the safety net and database replication as the thing that actually keeps me asleep at night.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does it take to restore from S3 after a disaster?
&lt;/h3&gt;

&lt;p&gt;You will not know until you time it. Bandwidth to the S3 endpoint, I/O on the restore target, and whether you are doing a full restore or point-in-time recovery all dominate. Run a quarterly drill, record the real number in the runbook, and use that as your RTO. The planning-doc number is fiction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use AWS S3 or self-hosted S3 for DR?
&lt;/h3&gt;

&lt;p&gt;Both work. AWS S3 with Cross-Region Replication is simpler to configure but costs more (storage plus request and egress fees) and keeps your DR data inside the same vendor blast radius as production. Self-hosted S3 (RustFS, MinIO, Ceph RGW) gives lower ongoing cost, no inter-site egress billing, and vendor independence, at the price of operating the DR storage yourself. Many teams run a hybrid: self-hosted for the primary site, a cloud bucket as the off-site archive tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does S3 Cross-Region Replication protect me from accidental deletion?
&lt;/h3&gt;

&lt;p&gt;Not by itself. CRR faithfully replicates deletions when &lt;code&gt;DeleteMarkerReplication&lt;/code&gt; is enabled, so a mass-delete on the primary propagates to DR. Real protection comes from &lt;strong&gt;versioning&lt;/strong&gt; plus &lt;strong&gt;Object Lock&lt;/strong&gt; (WORM retention) on the destination bucket, and from keeping at least one backup copy outside the replication path. Replication is a redundancy mechanism, not a backup.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the required IAM setup for &lt;code&gt;put-bucket-replication&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Role&lt;/code&gt; field is a required top-level element of the replication configuration — it is the IAM role S3 assumes on your behalf. That role needs read permissions plus &lt;code&gt;s3:GetReplicationConfiguration&lt;/code&gt; on the source bucket, and &lt;code&gt;s3:ReplicateObject&lt;/code&gt;, &lt;code&gt;s3:ReplicateDelete&lt;/code&gt; and &lt;code&gt;s3:ReplicateTags&lt;/code&gt; on the destination. Versioning must be enabled on both buckets before the rule will apply.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;All figures and commands in this article were checked against primary sources on &lt;strong&gt;2026-08-06&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uptime Institute, &lt;em&gt;2022 Outage Analysis&lt;/em&gt; — outage cost and human-error figures: &lt;a href="https://www.businesswire.com/news/home/20220608005265/en/" rel="noopener noreferrer"&gt;https://www.businesswire.com/news/home/20220608005265/en/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS CLI reference, &lt;code&gt;put-bucket-replication&lt;/code&gt; (required &lt;code&gt;Role&lt;/code&gt; field, versioning prerequisite): &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-replication.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-replication.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;rclone S3 backend configuration: &lt;a href="https://rclone.org/s3/" rel="noopener noreferrer"&gt;https://rclone.org/s3/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;rclone &lt;code&gt;sync&lt;/code&gt; command semantics: &lt;a href="https://rclone.org/commands/rclone_sync/" rel="noopener noreferrer"&gt;https://rclone.org/commands/rclone_sync/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RustFS README, Feature &amp;amp; Status table + Apache 2.0 license: &lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;https://github.com/rustfs/rustfs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Commands marked &lt;code&gt;NOT EXECUTED IN CI&lt;/code&gt; are reproduced verbatim from the linked official documentation but were not run in the environment used to write this article.&lt;/p&gt;

</description>
      <category>s3</category>
      <category>disasterrecovery</category>
      <category>backup</category>
      <category>rustfs</category>
    </item>
    <item>
      <title>S3 Lifecycle Policies: How to Cut Your Storage Bill 40% (With the Actual Arithmetic)</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:14:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/s3-lifecycle-policies-how-to-cut-your-storage-bill-40-with-the-actual-arithmetic-4lnk</link>
      <guid>https://dev.to/ethan-carter/s3-lifecycle-policies-how-to-cut-your-storage-bill-40-with-the-actual-arithmetic-4lnk</guid>
      <description>&lt;p&gt;An S3 lifecycle policy moves objects between storage classes on a schedule, or deletes them. The '40%' people quote is real, but only for one specific bucket shape, and only on the storage line. On AWS us-east-1 list prices, pushing 30% of a 100 TB bucket to Standard-IA and 40% to Glacier Instant Retrieval drops the storage line from &lt;strong&gt;$2,304.00 to $1,254.40 — a 45.6% cut&lt;/strong&gt;. The arithmetic is the boring part. The saving leaks back out in the quarter after you ship the rule, in the line items nobody re-opens.&lt;/p&gt;

&lt;p&gt;Every number here is pulled from the &lt;strong&gt;AWS Price List Bulk API&lt;/strong&gt; (&lt;code&gt;us-east-1&lt;/code&gt; offer file, &lt;code&gt;publicationDate 2026-08-07&lt;/code&gt;) or worked out from those rates. If a rate isn't in the offer file, I say so. I'm not inventing a number to fill the gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Stats&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Figure&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3 Standard, first 50 TB/mo&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.023 / GB-mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS Price List API, us-east-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Standard-IA&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.0125 / GB-mo&lt;/strong&gt; + $0.01/GB retrieval&lt;/td&gt;
&lt;td&gt;AWS Price List API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Glacier Instant Retrieval&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.004 / GB-mo&lt;/strong&gt; + $0.03/GB retrieval&lt;/td&gt;
&lt;td&gt;AWS Price List API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Glacier Flexible Retrieval&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0036 / GB-mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS Price List API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimum object size that transitions by default&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;128 KB&lt;/strong&gt; (since Sept 2024)&lt;/td&gt;
&lt;td&gt;AWS S3 User Guide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimum storage duration&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;30 d&lt;/strong&gt; (IA) / &lt;strong&gt;90 d&lt;/strong&gt; (Glacier IR, Flexible) / &lt;strong&gt;180 d&lt;/strong&gt; (Deep Archive)&lt;/td&gt;
&lt;td&gt;AWS S3 User Guide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle transition request, into Glacier Flexible&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.03 / 1,000 requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS Price List API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What is an S3 lifecycle policy?
&lt;/h2&gt;

&lt;p&gt;An S3 lifecycle policy is a bucket-level ruleset: it transitions objects to a cheaper class, or expires them, once they hit an age you set. You attach JSON or XML; S3 runs it asynchronously, so there's no cron job to babysit.&lt;/p&gt;

&lt;p&gt;The User Guide lists five actions: &lt;code&gt;Transition&lt;/code&gt;, &lt;code&gt;Expiration&lt;/code&gt;, &lt;code&gt;NoncurrentVersionTransition&lt;/code&gt;, &lt;code&gt;NoncurrentVersionExpiration&lt;/code&gt;, &lt;code&gt;AbortIncompleteMultipartUpload&lt;/code&gt;, and a &lt;code&gt;Filter&lt;/code&gt; that takes &lt;code&gt;Prefix&lt;/code&gt;, &lt;code&gt;Tag&lt;/code&gt;, &lt;code&gt;ObjectSizeGreaterThan&lt;/code&gt;, &lt;code&gt;ObjectSizeLessThan&lt;/code&gt;, or &lt;code&gt;And&lt;/code&gt; to combine them.&lt;/p&gt;

&lt;p&gt;Two things actually cost you money. One: transitions only go down the ladder. The User Guide is blunt that the Deep Archive transition 'can go only one way.' Two: billing starts the instant the rule is satisfied, not when the data finishes moving. You're paying the destination rate from day one, even if AWS is still copying in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does the 40% actually come from?
&lt;/h2&gt;

&lt;p&gt;It comes down to the price gap between classes, times how much of your data is cold. That's the whole formula. For a &lt;strong&gt;100 TB bucket in us-east-1&lt;/strong&gt; (AWS bills in 1 TB = 1,024 GB, so that's 102,400 GB), baseline, everything in S3 Standard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 51,200 GB × $0.023  = $1,177.60   (first 50 TB band)
 51,200 GB × $0.022  = $1,126.40   (next 450 TB band)
 ------------------------------------------------
 TOTAL                 $2,304.00 / month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Split that 100 TB three ways: hot in Standard, warm in Standard-IA, cold in Glacier IR:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Hot / Warm / Cold&lt;/th&gt;
&lt;th&gt;Monthly storage&lt;/th&gt;
&lt;th&gt;Saving&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Conservative&lt;/td&gt;
&lt;td&gt;50% / 30% / 20%&lt;/td&gt;
&lt;td&gt;$1,643.52&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−28.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical&lt;/td&gt;
&lt;td&gt;30% / 30% / 40%&lt;/td&gt;
&lt;td&gt;$1,254.40&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−45.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggressive&lt;/td&gt;
&lt;td&gt;20% / 20% / 60%&lt;/td&gt;
&lt;td&gt;$972.80&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−57.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That 45.6% assumes roughly two-thirds of your bytes are over 30 days old, which is the usual shape for logs, backups, and ML snapshots. But it's the storage line only. Requests and retrievals bill on top, and they're exactly where the saving disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which storage class should each tier of data land in?
&lt;/h2&gt;

&lt;p&gt;Pick the class by how often you actually read the data, then check the minimum duration. The two numbers that drive cost are per-GB storage and per-GB retrieval. Here's the full ladder from the same offer file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage class&lt;/th&gt;
&lt;th&gt;$/GB-mo&lt;/th&gt;
&lt;th&gt;Retrieval&lt;/th&gt;
&lt;th&gt;Min duration&lt;/th&gt;
&lt;th&gt;Min billable size&lt;/th&gt;
&lt;th&gt;Availability SLA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3 Standard&lt;/td&gt;
&lt;td&gt;$0.023&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;99.99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Standard-IA&lt;/td&gt;
&lt;td&gt;$0.0125&lt;/td&gt;
&lt;td&gt;$0.01/GB&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;128 KB&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 One Zone-IA&lt;/td&gt;
&lt;td&gt;$0.010&lt;/td&gt;
&lt;td&gt;$0.01/GB&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;128 KB&lt;/td&gt;
&lt;td&gt;99.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Glacier Instant Retrieval&lt;/td&gt;
&lt;td&gt;$0.004&lt;/td&gt;
&lt;td&gt;$0.03/GB&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;128 KB&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Glacier Flexible Retrieval&lt;/td&gt;
&lt;td&gt;$0.0036&lt;/td&gt;
&lt;td&gt;$0.01/GB standard, $0.00/GB bulk&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;n/a*&lt;/td&gt;
&lt;td&gt;99.99% after restore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Glacier Deep Archive&lt;/td&gt;
&lt;td&gt;see note&lt;/td&gt;
&lt;td&gt;$0.02/GB standard, $0.0025/GB bulk&lt;/td&gt;
&lt;td&gt;180 days&lt;/td&gt;
&lt;td&gt;n/a*&lt;/td&gt;
&lt;td&gt;99.99% after restore&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* Glacier Flexible and Deep Archive add &lt;strong&gt;40 KB of overhead per object&lt;/strong&gt;, of which 8 KB is billed at S3 Standard rates and 32 KB at the destination Glacier rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note on Deep Archive:&lt;/strong&gt; the us-east-1 S3 offer file exposes Deep Archive's transition, retrieval, and checksum SKUs but &lt;strong&gt;no per-GB timed-storage SKU&lt;/strong&gt;. I won't quote a storage rate I can't pull from a first-party file, so get it from the pricing console before you budget on it. The closest verifiable neighbour in the same file is Intelligent-Tiering's Deep Archive Access tier at &lt;strong&gt;$0.00099/GB-mo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One Zone-IA is 20% cheaper and lives in a single AZ at 99.5% availability. Fine for regenerable stuff: thumbnails, transcodes, a second backup copy. Never the only copy you've got.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you write and apply the rule?
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;aws s3api put-bucket-lifecycle-configuration&lt;/code&gt;. This is the official AWS CLI example, copied verbatim from the command reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-bucket-lifecycle-configuration &lt;span class="nt"&gt;--bucket&lt;/span&gt; amzn-s3-demo-bucket &lt;span class="nt"&gt;--lifecycle-configuration&lt;/span&gt;  file://lifecycle.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Move rotated logs to Glacier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rotated/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Transitions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"Date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2015-11-10T00:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"StorageClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GLACIER"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"NoncurrentVersionTransitions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"NoncurrentDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"StorageClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GLACIER"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Move old versions to Glacier"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;[Both blocks sourced verbatim from docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-lifecycle-configuration.html — NOT EXECUTED IN CI.]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A workable rule, using only documented &lt;code&gt;LifecycleRule&lt;/code&gt; fields: it tiers logs down, skips objects too small to move, expires old versions, and reclaims abandoned multipart uploads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logs-tier-down"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"And"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logs/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ObjectSizeGreaterThan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;131072&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Transitions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"StorageClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STANDARD_IA"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"StorageClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GLACIER_IR"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"NoncurrentVersionExpiration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"NoncurrentDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"AbortIncompleteMultipartUpload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"DaysAfterInitiation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ObjectSizeGreaterThan: 131072&lt;/code&gt; is just 128 KB written out, and the next section explains why that number matters. The Glacier IR hop sits at day 120, not 90, because Standard-IA has a 30-day minimum and the User Guide says the second transition 'must occur after at least' the first one's minimum has passed. Tighten that gap and AWS rejects the rule outright.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do small objects break lifecycle economics?
&lt;/h2&gt;

&lt;p&gt;You pay per transition request, not per byte. The first lifecycle bill that ever surprised me was a 200 KB-heavy bucket where transition cost ate the whole storage saving, which is the trap most people hit before they check object size. A 200 KB object costs the same request as a 200 MB one. AWS made that the default in September 2024: 'the default behavior prevents objects smaller than 128 KB from being transitioned to any storage class.'&lt;/p&gt;

&lt;p&gt;Payback (transition fee divided by the monthly saving) at list rates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object size&lt;/th&gt;
&lt;th&gt;→ Standard-IA&lt;/th&gt;
&lt;th&gt;→ Glacier IR&lt;/th&gt;
&lt;th&gt;→ Glacier Flexible&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;128 KB&lt;/td&gt;
&lt;td&gt;7.8 months&lt;/td&gt;
&lt;td&gt;8.6 months&lt;/td&gt;
&lt;td&gt;12.7 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256 KB&lt;/td&gt;
&lt;td&gt;3.9 months&lt;/td&gt;
&lt;td&gt;4.3 months&lt;/td&gt;
&lt;td&gt;6.3 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 MB&lt;/td&gt;
&lt;td&gt;1.0 month&lt;/td&gt;
&lt;td&gt;1.1 months&lt;/td&gt;
&lt;td&gt;1.6 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 MB&lt;/td&gt;
&lt;td&gt;~0.1 month&lt;/td&gt;
&lt;td&gt;~0.1 month&lt;/td&gt;
&lt;td&gt;~0.2 months&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scale that to a whole bucket. Same 100 TB, one transition each, at $0.01/1,000 into Standard-IA and $0.03/1,000 into Glacier Flexible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Average object size&lt;/th&gt;
&lt;th&gt;Object count&lt;/th&gt;
&lt;th&gt;One-time bill → Standard-IA&lt;/th&gt;
&lt;th&gt;→ Glacier Flexible&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;64 MB&lt;/td&gt;
&lt;td&gt;1,638,400&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$16.38&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$49.15&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 MB&lt;/td&gt;
&lt;td&gt;52,428,800&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$524.29&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1,572.86&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 KB&lt;/td&gt;
&lt;td&gt;536,870,912&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5,368.71&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$16,106.13&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same 100 TB, but a 328x swing in transition cost driven purely by object size. AWS says it straight: 'for smaller objects, the transition costs can outweigh the storage savings.' Check your average object size in S3 Storage Lens first, and put &lt;code&gt;ObjectSizeGreaterThan&lt;/code&gt; on every transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the policy cost when the data comes back?
&lt;/h2&gt;

&lt;p&gt;Retrieval fees are the second place the win leaks out. Standard-IA saves $0.0105/GB-mo versus Standard but charges $0.01/GB to read. Those two numbers give you a hard break-even:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Monthly saving vs Standard&lt;/th&gt;
&lt;th&gt;Retrieval&lt;/th&gt;
&lt;th&gt;Break-even&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard-IA&lt;/td&gt;
&lt;td&gt;$0.0105/GB&lt;/td&gt;
&lt;td&gt;$0.01/GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.05 full reads/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One Zone-IA&lt;/td&gt;
&lt;td&gt;$0.0130/GB&lt;/td&gt;
&lt;td&gt;$0.01/GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.30 full reads/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glacier Instant Retrieval&lt;/td&gt;
&lt;td&gt;$0.0190/GB&lt;/td&gt;
&lt;td&gt;$0.03/GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.63 full reads/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read a Glacier IR object more than about twice a quarter and you'd have been cheaper in Standard. The '$0.004/GB' headline hides how tight that budget really is.&lt;/p&gt;

&lt;p&gt;Early deletion is the third trap. Delete or overwrite before the minimum and AWS bills the rest pro-rated: $0.0125/GB-mo for Standard-IA, $0.004 for Glacier IR, $0.0036 for Glacier. Point a 30-day retention rule at a 90-day-minimum class and you pay for 90 days of Glacier every month, indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intelligent-Tiering or hand-written rules?
&lt;/h2&gt;

&lt;p&gt;Intelligent-Tiering adds a &lt;strong&gt;monitoring fee of $0.0025 per 1,000 objects per month&lt;/strong&gt; and moves data between tiers for you, with no retrieval fee on the Frequent, Infrequent, or Archive Instant tiers. Hand-written rules are free to run but assume you already know your access pattern.&lt;/p&gt;

&lt;p&gt;What decides it is object count, not total data:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object count&lt;/th&gt;
&lt;th&gt;Intelligent-Tiering monitoring&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;$2.50/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50,000,000&lt;/td&gt;
&lt;td&gt;$125.00/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;536,870,912&lt;/td&gt;
&lt;td&gt;$1,342.18/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I use Intelligent-Tiering for buckets with unpredictable reads and big objects, and explicit rules where the access pattern is already obvious. Logs and backups are the obvious case: nobody reads last quarter's access logs spontaneously, so a monitoring fee to find that out is pure waste. The other direction: half a billion thumbnails is $1,342/month in monitoring, and most of those objects sit under the 128 KB threshold anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does lifecycle work the same on self-hosted S3?
&lt;/h2&gt;

&lt;p&gt;Not quite. On AWS a transition shuffles bytes between AWS-run classes. On self-hosted S3 there's no Glacier, so a 'lifecycle rule' means expire, clean up old versions, or move to a remote tier you set up yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MinIO&lt;/strong&gt; implements this through &lt;code&gt;mc ilm&lt;/code&gt;. From the official command reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mc ilm rule add &lt;span class="nt"&gt;--expire-days&lt;/span&gt; 90 &lt;span class="nt"&gt;--noncurrent-expire-days&lt;/span&gt; 30  myaistor/mydata

mc ilm rule add &lt;span class="nt"&gt;--transition-days&lt;/span&gt; 30 &lt;span class="nt"&gt;--transition-tier&lt;/span&gt; &lt;span class="s2"&gt;"COLDTIER"&lt;/span&gt; myaistor/mydata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;[Verbatim from min.io/docs — the &lt;code&gt;myaistor&lt;/code&gt; alias is MinIO's own example alias. NOT EXECUTED IN CI.]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RustFS&lt;/strong&gt; is straight about this one. Its README Feature &amp;amp; Status table shows &lt;strong&gt;Lifecycle Management as 🚧 Under Testing&lt;/strong&gt;, next to Distributed Mode and RustFS KMS. Versioning, Bucket Replication, Event Notifications, Bitrot Protection, and Single Node Mode are ✅ available; there's no Object Lock row and no tiering row. If tiered lifecycle is a hard requirement today, RustFS isn't the answer yet.&lt;/p&gt;

&lt;p&gt;What RustFS does give you is the structural argument: your own disks mean no per-GB retrieval fee and no per-request transition fee, so the break-even math above stops applying. The catch is you buy and run the hardware. If you're already running RustFS for other reasons, expire-and-cleanup lifecycle is free and worth turning on. If you'd stand it up purely to dodge egress, the hardware usually costs more than the bandwidth you save, so run the numbers first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 9000:9000 &lt;span class="nt"&gt;-p&lt;/span&gt; 9001:9001 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/logs:/logs rustfs/rustfs:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;[Verbatim from github.com/rustfs/rustfs README — NOT EXECUTED IN CI.]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;S3 API on &lt;code&gt;:9000&lt;/code&gt;, Console on &lt;code&gt;:9001&lt;/code&gt;. The README lists default credentials &lt;code&gt;rustfsadmin&lt;/code&gt; / &lt;code&gt;rustfsadmin&lt;/code&gt;, and docs.rustfs.com explicitly tells you not to keep them: "Do not use the well-known &lt;code&gt;rustfsadmin&lt;/code&gt; value for either credential."&lt;/p&gt;

&lt;h2&gt;
  
  
  The five mistakes that eat the savings
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Transitioning objects under 128 KB.&lt;/strong&gt; Payback is 8–13 months. Filter them out with &lt;code&gt;ObjectSizeGreaterThan&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chaining transitions inside a minimum duration.&lt;/strong&gt; Standard-IA → Glacier IR at day 60 is invalid; the IA minimum is 30 days and Glacier IR's own 90-day clock only starts on arrival.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;NoncurrentVersionExpiration&lt;/code&gt;.&lt;/strong&gt; On a versioned bucket, every overwrite leaves a billable version. Storage grows forever while your object count looks flat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping &lt;code&gt;AbortIncompleteMultipartUpload&lt;/code&gt;.&lt;/strong&gt; Failed uploads leave parts that are invisible in &lt;code&gt;ListObjects&lt;/code&gt; but fully billable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enabling Intelligent-Tiering on hundreds of millions of small objects.&lt;/strong&gt; Objects under 128 KB are never monitored for tiering, but you can still be paying monitoring fees at scale for the rest.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How much can S3 lifecycle policies actually save?
&lt;/h3&gt;

&lt;p&gt;On AWS us-east-1 list prices, a 100 TB bucket costs $2,304.00/month entirely in S3 Standard. Moving 30% to Standard-IA and 40% to Glacier Instant Retrieval brings it to $1,254.40/month — a 45.6% cut. A conservative 50/30/20 split still saves 28.7%. Savings scale linearly with the fraction of cold data, and apply to the storage line only.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the minimum object size for an S3 lifecycle transition?
&lt;/h3&gt;

&lt;p&gt;128 KB. Since September 2024, AWS's default behavior prevents objects smaller than 128 KB from transitioning to any storage class. Configurations created before September 2024 keep the old behavior until you edit them. You can override with the &lt;code&gt;x-amz-transition-default-minimum-object-size&lt;/code&gt; header on &lt;code&gt;PutBucketLifecycleConfiguration&lt;/code&gt;, but you usually should not: a 128 KB object takes 7.8 months to pay back a single Standard-IA transition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does S3 charge for lifecycle transitions?
&lt;/h3&gt;

&lt;p&gt;Yes, per request. In us-east-1: $0.01 per 1,000 transitions into Standard-IA, One Zone-IA, or Intelligent-Tiering; $0.02 per 1,000 into Glacier Instant Retrieval; $0.03 per 1,000 into Glacier Flexible Retrieval; $0.05 per 1,000 into Glacier Deep Archive. For 536 million small objects, that single move to Glacier Flexible is $16,106.&lt;/p&gt;

&lt;h3&gt;
  
  
  When is Standard-IA more expensive than S3 Standard?
&lt;/h3&gt;

&lt;p&gt;When you read the object more than 1.05 times per month. Standard-IA saves $0.0105/GB-month on storage but charges $0.01/GB on retrieval, so a little over one full read per month erases the discount. Glacier Instant Retrieval breaks even at 0.63 reads/month because its $0.03/GB retrieval fee is three times higher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do self-hosted S3 servers support lifecycle policies?
&lt;/h3&gt;

&lt;p&gt;Partially, and support varies. MinIO implements lifecycle through &lt;code&gt;mc ilm rule add&lt;/code&gt; with expiration and remote-tier transition. RustFS lists Lifecycle Management as 🚧 Under Testing in its README Feature &amp;amp; Status table, so it is not production-ready today. The upside of self-hosting is that per-GB retrieval fees and per-request transition fees do not exist, which removes the break-even math entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;All figures checked &lt;strong&gt;2026-08-10&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS Price List Bulk API — &lt;code&gt;pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonS3/current/region_index.json&lt;/code&gt;, us-east-1 offer file, &lt;code&gt;publicationDate 2026-08-07&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;All per-GB storage, retrieval, request, and transition rates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-transition-general-considerations.html" rel="noopener noreferrer"&gt;Transitioning objects — general considerations&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;128 KB default, minimum durations, 40 KB Glacier overhead, one-way Deep Archive, early-billing rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html" rel="noopener noreferrer"&gt;Understanding and managing Amazon S3 storage classes&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Minimum billable size, availability SLAs, AZ counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/intro-lifecycle-rules.html" rel="noopener noreferrer"&gt;S3 Lifecycle configuration elements&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Action and Filter element names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-lifecycle-configuration.html" rel="noopener noreferrer"&gt;aws s3api put-bucket-lifecycle-configuration&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Verbatim CLI command and JSON example&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://min.io/docs/minio/linux/reference/minio-mc/mc-ilm-rule-add.html" rel="noopener noreferrer"&gt;mc ilm rule add&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Verbatim MinIO ILM commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;rustfs/rustfs README&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Verbatim docker run, default credentials, Feature &amp;amp; Status table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.rustfs.com/en/installation/container/docker" rel="noopener noreferrer"&gt;docs.rustfs.com — Docker&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;API port 9000, Console port 9001, credential warning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Before you trust any of this on your own bucket, pull object count and average size from S3 Storage Lens and drop them into the payback table to see whether a rule actually pays off. In my experience most don't, or only barely.&lt;/p&gt;

&lt;p&gt;If retrieval and transition fees, not the storage rate, are what's blowing up your bill, self-hosted S3-compatible storage deletes both line items. &lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt; is Apache 2.0, S3-compatible, and runs single-node today: &lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;github.com/rustfs/rustfs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>devops</category>
      <category>storage</category>
    </item>
    <item>
      <title>Cross-Region S3 Replication Without the Gotchas (2026)</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Mon, 17 Aug 2026 13:07:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/cross-region-s3-replication-without-the-gotchas-2026-f3l</link>
      <guid>https://dev.to/ethan-carter/cross-region-s3-replication-without-the-gotchas-2026-f3l</guid>
      <description>&lt;h1&gt;
  
  
  Cross-Region S3 Replication Without the Gotchas (2026)
&lt;/h1&gt;

&lt;p&gt;Cross-region S3 replication copies objects from a source bucket to a destination in another region, continuously. AWS S3, MinIO, and RustFS all do it, but the failures almost never come from the copy itself. They come from versioning, IAM, and delete-marker defaults you forgot to set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key facts before you start
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What the official docs say&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Versioning&lt;/td&gt;
&lt;td&gt;AWS requires versioning &lt;strong&gt;enabled on both&lt;/strong&gt; the source and destination buckets before replication works.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-existing objects&lt;/td&gt;
&lt;td&gt;AWS does &lt;strong&gt;not&lt;/strong&gt; replicate objects written before the replication configuration; use S3 Batch Replication to backfill.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM role&lt;/td&gt;
&lt;td&gt;AWS replication needs a role with 8 S3 actions (GetReplicationConfiguration, ListBucket, GetObjectVersionForReplication/Acl/Tagging, ReplicateObject/Delete/Tags).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MinIO&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mc replicate add&lt;/code&gt; requires versioning on the source bucket; objects without a version ID are excluded.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RustFS&lt;/td&gt;
&lt;td&gt;Site replication (&lt;code&gt;rc admin replicate add&lt;/code&gt;) links whole deployments and needs versioning + TLS + root creds at every site.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What is cross-region S3 replication, in plain terms?
&lt;/h2&gt;

&lt;p&gt;CRR is a server-side rule that copies every new object from a source bucket to a destination in another region, asynchronously. 'Asynchronously' matters here: the source write returns success before the copy lands, so the two buckets are eventually consistent, not locked in step. Anything that speaks the &lt;code&gt;PUT Bucket Replication&lt;/code&gt; API does the same: AWS, MinIO, RustFS. The point is disaster recovery and reads closer to the user. But 'just copy my bucket to another region' hides a dozen small defaults. Versioning state, IAM trust, delete-marker handling, KMS keys: each one fails silently instead of throwing an error you'd notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioning is the prerequisite nobody can skip
&lt;/h2&gt;

&lt;p&gt;This is the one I see missed most in postmortems. Versioning has to be on, at both ends, or replication doesn't work. I've spent multiple sessions debugging broken replication from skipping the 15-minute propagation wait after enabling versioning, so now I turn it on and walk away before writing anything. AWS tracks objects by version ID, so the destination needs it too. Enabling it is one CLI call (verbatim below). AWS flags a gotcha here that's worth taking seriously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enable versioning on BOTH the source and destination bucket&lt;/span&gt;
aws s3api put-bucket-versioning &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bucket&lt;/span&gt; amzn-s3-demo-bucket &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;AWS note: "When you enable versioning on a bucket for the first time, it might take a short amount of time for the change to be fully propagated... We recommend that you wait for 15 minutes after enabling versioning before issuing write operations."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you attach the rule to a bucket without versioning, AWS rejects the config outright. If only the destination is missing it, the source accepts writes but every replica write fails silently, and nothing shows up remotely. That's the failure I get pinged about more than any other: replication looks healthy, the destination is just empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you set up the AWS IAM role without a silent AccessDenied?
&lt;/h2&gt;

&lt;p&gt;S3 assumes an IAM role to do the copy. The role has two halves: a trust policy letting &lt;code&gt;s3.amazonaws.com&lt;/code&gt; assume it, and a permissions policy granting the exact replication actions. Both blocks below are verbatim from the AWS replication permissions docs. The trust policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"s3.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The permissions policy (source bucket = &lt;code&gt;amzn-s3-demo-source-bucket&lt;/code&gt;, destination = &lt;code&gt;amzn-s3-demo-destination-bucket&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetReplicationConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::amzn-s3-demo-source-bucket"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObjectVersionForReplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObjectVersionAcl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObjectVersionTagging"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::amzn-s3-demo-source-bucket/*"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"s3:ReplicateObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"s3:ReplicateDelete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"s3:ReplicateTags"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::amzn-s3-demo-destination-bucket/*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where this goes wrong is quiet: drop the &lt;code&gt;s3.amazonaws.com&lt;/code&gt; principal from the trust policy, or leave &lt;code&gt;s3:ReplicateDelete&lt;/code&gt; out of the permissions, and replication just doesn't happen. No CloudTrail error, no SNS alert; the destination stays empty. Check the role ARN in the replication config matches what you attached, then test with one object.&lt;/p&gt;

&lt;h2&gt;
  
  
  The replication configuration file, field by field
&lt;/h2&gt;

&lt;p&gt;With versioning and the role set, you attach a replication config to the source bucket. The command is verbatim from the AWS CLI reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-bucket-replication &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bucket&lt;/span&gt; amzn-s3-demo-bucket1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--replication-configuration&lt;/span&gt; file://replication.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The minimal &lt;code&gt;replication.json&lt;/code&gt; (verbatim from AWS docs) already bakes in two defaults that bite people:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::123456789012:role/s3-replication-role"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"DeleteMarkerReplication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Disabled"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Filter"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::amzn-s3-demo-bucket2"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty &lt;code&gt;Filter&lt;/code&gt; prefix means 'all objects'; &lt;code&gt;Priority&lt;/code&gt; decides which rule wins when two rules match the same object. The two fields worth adding for production are &lt;code&gt;Metrics&lt;/code&gt; / &lt;code&gt;ReplicationTime&lt;/code&gt;, which switch on lag tracking. Note they live inside the &lt;code&gt;Destination&lt;/code&gt; block, per the AWS schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::123456789012:role/s3-replication-role"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replicate-all-with-metrics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"DeleteMarkerReplication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Disabled"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::amzn-s3-demo-bucket2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Metrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"EventThreshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"ReplicationTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Metrics&lt;/code&gt; and &lt;code&gt;ReplicationTime&lt;/code&gt; are real fields in the AWS replication schema; enabling them is what makes replication lag visible instead of invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are your old objects missing from the replica?
&lt;/h2&gt;

&lt;p&gt;This trips up almost everyone, and it's burned me on migrations that looked green until someone noticed six months of history missing from the replica. AWS says it plainly: 'Objects that existed before you set up replication aren't replicated automatically. In other words, Amazon S3 doesn't replicate objects retroactively.' A rule you add today only covers objects written or changed after it goes live. That 2 TB of history already in the source? It sits there until you backfill it explicitly.&lt;/p&gt;

&lt;p&gt;On AWS, the supported way to handle that is &lt;strong&gt;S3 Batch Replication&lt;/strong&gt;, a one-time job that copies existing objects on demand (the older per-rule &lt;code&gt;ExistingObjectReplication&lt;/code&gt; option is no longer supported — AWS removed it and points to Batch Replication). Note that backfilling is a large, billable operation for cross-region transfers. MinIO does the same through the &lt;code&gt;--replicate&lt;/code&gt; flag instead of a JSON field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mc replicate add myaistor/mybucket &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--remote-bucket&lt;/span&gt; https://user:secret@minio.mysite.tld/remotebucket &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--replicate&lt;/span&gt; &lt;span class="s2"&gt;"delete,delete-marker,existing-objects"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;existing-objects&lt;/code&gt; value tells MinIO to replicate objects already there. MinIO needs versioning on first: objects written before versioning has no version ID and get excluded. Check the current MinIO docs for the exact &lt;code&gt;mc version&lt;/code&gt; subcommand before running that, since the flag name drifts between versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delete markers and KMS keys: the two settings that bite later
&lt;/h2&gt;

&lt;p&gt;Delete markers and KMS encryption both default to the safe-but-surprising option, and neither one errors when it's wrong. First, delete markers. With versioning on, deleting an object writes a delete marker instead of removing data. &lt;code&gt;DeleteMarkerReplication&lt;/code&gt; is &lt;code&gt;Disabled&lt;/code&gt; by default, so a delete in the source does &lt;strong&gt;not&lt;/strong&gt; delete in the destination, so your DR replica keeps serving the 'deleted' object. Set &lt;code&gt;"DeleteMarkerReplication": { "Status": "Enabled" }&lt;/code&gt; if you want deletes to follow, but in a bidirectional setup that can trigger delete storms, so decide on purpose. From production experience I rarely run bidirectional replication with delete-marker sync on; a delete storm across sites is painful and slow to unwind, so I keep delete propagation off and clean up the destination by hand when something is actually gone.&lt;/p&gt;

&lt;p&gt;Second, KMS-encrypted objects. If the source uses SSE-KMS, replication needs &lt;code&gt;SourceSelectionCriteria.SseKmsEncryptedObjects.Status = "Enabled"&lt;/code&gt; plus a &lt;code&gt;Destination.EncryptionConfiguration.ReplicaKmsKeyID&lt;/code&gt;, and the IAM role has to be allowed &lt;code&gt;kms:Decrypt&lt;/code&gt; on the source key and &lt;code&gt;kms:Encrypt&lt;/code&gt; on the destination key (&lt;code&gt;kms:GenerateDataKey&lt;/code&gt; is only needed when you replicate plaintext objects into a destination bucket that has default SSE-KMS/DSSE-KMS encryption). Miss any of that and encrypted objects never show up at the destination while unencrypted ones do. That's a partial, silent failure that's a pain to diagnose. Safest move until the KMS grants are confirmed: keep the same key, or SSE-S3, on both ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do self-hosted engines replicate across regions?
&lt;/h2&gt;

&lt;p&gt;If you run your own S3-compatible storage, the model holds: a rule or a site link copies objects between two endpoints, but the tooling is different. On MinIO, the &lt;code&gt;mc replicate add&lt;/code&gt; above is the whole flow (after versioning). On RustFS, cross-region is &lt;strong&gt;site replication&lt;/strong&gt;, which links two or more independent deployments and syncs buckets, object versions, and IAM across them. The commands are from the RustFS &lt;code&gt;rc&lt;/code&gt; client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Configure one alias per site (repeat for each deployment)&lt;/span&gt;
rc &lt;span class="nb"&gt;alias set &lt;/span&gt;site1 https://site1.example.com:9000 &lt;span class="se"&gt;\&lt;/span&gt;
    &amp;lt;your-access-key&amp;gt; &amp;lt;your-secret-key&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--bucket-lookup&lt;/span&gt; path

&lt;span class="c"&gt;# Confirm both sites are reachable&lt;/span&gt;
rc ready site1

&lt;span class="c"&gt;# Link the sites (first alias receives the admin request)&lt;/span&gt;
rc admin replicate add site1 site2

&lt;span class="c"&gt;# Inspect replication status from either site&lt;/span&gt;
rc admin replicate status site1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RustFS is explicit about what this needs: two or more independent deployments, a stable S3 API endpoint per site, bidirectional connectivity on the S3 API port (normally &lt;code&gt;9000&lt;/code&gt;), trusted TLS certs, the &lt;code&gt;rc&lt;/code&gt; client on a secured host, root admin creds at every site, and bucket-versioning support at every site. Bring each deployment up with the verified Docker command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 9000:9000 &lt;span class="nt"&gt;-p&lt;/span&gt; 9001:9001 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/logs:/logs rustfs/rustfs:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default console credentials are &lt;code&gt;rustfsadmin&lt;/code&gt; / &lt;code&gt;rustfsadmin&lt;/code&gt;, and the console listens on port &lt;code&gt;9001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The RustFS docs are clear about the limits: site replication is &lt;strong&gt;asynchronous&lt;/strong&gt; (a successful write at one site doesn't mean the other already has it) and provides &lt;strong&gt;no DNS failover, no traffic routing, no application recovery orchestration&lt;/strong&gt;. Plan your own recovery. Distributed Mode is still 'Under Testing' in the Feature &amp;amp; Status table, so treat each linked site as single-node-ready today, not a multi-node cluster, and test the workflow on empty sites before you link production data. RustFS gives you cross-region sync, but not a turnkey active-active setup that resolves conflicts. The docs don't pretend otherwise. If replication is the only reason you'd stand up a second cluster, do the egress math first: the bandwidth you save is usually smaller than a second deployment's run cost, and self-hosting only wins when you already own the hardware or you have a data-residency requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring replication lag and the cost of cross-region traffic
&lt;/h2&gt;

&lt;p&gt;You can't see replication lag until you turn on metrics. On AWS that means setting &lt;code&gt;Metrics&lt;/code&gt; and &lt;code&gt;ReplicationTime&lt;/code&gt; in the rule (above) and alarming on the CloudWatch metrics they produce, and without those blocks AWS emits no replication-latency metric at all. &lt;code&gt;ReplicationTime&lt;/code&gt; lets you request a target replication window in minutes; AWS also offers S3 Replication Time Control (S3 RTC) for workloads that need a contracted SLA (99.9% of objects replicate within 15 minutes). Alarm on sustained backlog, not a single sample. Async replication falls behind temporarily under bursty writes.&lt;/p&gt;

&lt;p&gt;On cost, cloud and self-hosted aren't a tweak apart, they're structurally different. AWS bills inter-region transfer for every replicated byte (rates vary by region pair), so a high-churn bucket racks up real egress. A self-hosted pair, RustFS via &lt;code&gt;rc admin replicate add&lt;/code&gt; or MinIO via &lt;code&gt;mc replicate add&lt;/code&gt;, moves bytes over your own network, so there's no per-GB cloud egress for the replication itself (you still pay your bandwidth provider). I'm not quoting a per-GB number here because AWS inter-region rates are region-pair-specific and move around. The point that holds: the engine choice decides who sends the egress bill, not whether bytes move.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does S3 cross-region replication copy existing objects?
&lt;/h3&gt;

&lt;p&gt;No. AWS states that objects created before the replication configuration is attached are not replicated automatically. To move historical data, run an S3 Batch Replication job (the older per-rule &lt;code&gt;ExistingObjectReplication&lt;/code&gt; option is no longer supported). MinIO covers the same case with the &lt;code&gt;existing-objects&lt;/code&gt; value in &lt;code&gt;mc replicate add --replicate&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does cross-region replication replicate delete markers?
&lt;/h3&gt;

&lt;p&gt;Not by default, and I'd leave it off unless you have a reason. In the AWS replication schema, &lt;code&gt;DeleteMarkerReplication&lt;/code&gt; defaults to &lt;code&gt;Disabled&lt;/code&gt;, so deleting an object in the source does not delete its replica. Flip it to &lt;code&gt;"Status": "Enabled"&lt;/code&gt; only when the destination is a true mirror and you accept that a bad delete now spreads to both sides.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the destination bucket need versioning for S3 replication?
&lt;/h3&gt;

&lt;p&gt;Yes, on both ends, no exceptions. Replication keys off version IDs, so the destination has to track them too. One gotcha: the change isn't instant. AWS says wait about 15 minutes after you flip versioning on before you start writing, or you'll replicate into a half-propagated bucket and wonder why objects are missing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can MinIO or RustFS do cross-region S3 replication?
&lt;/h3&gt;

&lt;p&gt;Yes. MinIO uses &lt;code&gt;mc replicate add&lt;/code&gt; with a &lt;code&gt;--replicate&lt;/code&gt; flag that controls delete, delete-marker, and existing-object replication. RustFS uses site replication via the &lt;code&gt;rc&lt;/code&gt; client: &lt;code&gt;rc admin replicate add site1 site2&lt;/code&gt; links two deployments and syncs buckets, object versions, and IAM. Both require versioning on the source and (for RustFS) bidirectional network access on the S3 API port.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor S3 replication lag?
&lt;/h3&gt;

&lt;p&gt;On AWS, enable the &lt;code&gt;Metrics&lt;/code&gt; and &lt;code&gt;ReplicationTime&lt;/code&gt; blocks inside the replication rule; only then does AWS emit replication-latency CloudWatch metrics you can alarm on. Without those blocks, lag is not exposed by default. RustFS exposes per-site replication status through &lt;code&gt;rc admin replicate status site1 --metrics&lt;/code&gt;, which reports backlog and metrics from each deployment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.rustfs.com/s3-cross-region-replication/" rel="noopener noreferrer"&gt;RustFS blog&lt;/a&gt;. If you want S3-compatible replication without the per-GB cloud egress, &lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt; is open source under Apache 2.0 and links regions through site replication. &lt;a href="https://docs.rustfs.com/en/operations/high-availability/site-replication" rel="noopener noreferrer"&gt;read the docs&lt;/a&gt; or &lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;download&lt;/a&gt; to try it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>s3</category>
      <category>replication</category>
      <category>crossregion</category>
      <category>disasterrecovery</category>
    </item>
    <item>
      <title>Backblaze B2 vs Self-Hosted S3: Which Saves More Money?</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Sun, 16 Aug 2026 13:21:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/backblaze-b2-vs-self-hosted-s3-which-saves-more-money-4gp8</link>
      <guid>https://dev.to/ethan-carter/backblaze-b2-vs-self-hosted-s3-which-saves-more-money-4gp8</guid>
      <description>&lt;h1&gt;
  
  
  Backblaze B2 vs Self-Hosted S3: Which Actually Saves More Money in 2026?
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Backblaze B2's pricing is about as simple as cloud storage gets: $0.00695/GB/month (~$6.95/TB/mo) for storage, no charge for uploads, and free egress up to 3× your stored capacity (then $0.01/GB).&lt;/strong&gt; No storage tiers, no retrieval classes, no egress spreadsheet. For backups, archives, and media libraries that combination tends to be the practical choice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simple pricing and low total cost are not the same thing once your data passes a certain size. This article compares where B2 wins, where self-hosted S3 (&lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt;, MinIO) wins, and the data volume where the two cross over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Stats (Backblaze figures verified 2026-08-04)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Figure&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B2 pay-as-you-go storage&lt;/td&gt;
&lt;td&gt;$6.95 / TB / month ($0.00695/GB)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.backblaze.com/cloud-storage/pricing" rel="noopener noreferrer"&gt;Backblaze pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2 egress&lt;/td&gt;
&lt;td&gt;Free up to 3× monthly storage, then $0.01/GB&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.backblaze.com/cloud-storage/pricing" rel="noopener noreferrer"&gt;Backblaze pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Always-free tier&lt;/td&gt;
&lt;td&gt;First 10 GB of storage&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.backblaze.com/cloud-storage/pricing" rel="noopener noreferrer"&gt;Backblaze pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactions&lt;/td&gt;
&lt;td&gt;Class A/B/C free; Class D $0.004 per 10,000 calls&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.backblaze.com/cloud-storage/pricing" rel="noopener noreferrer"&gt;Backblaze pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2 regions&lt;/td&gt;
&lt;td&gt;4 — US West, US East, EU Central, CA East&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.backblaze.com/computer-backup/docs/data-centers-and-data-regions" rel="noopener noreferrer"&gt;Backblaze docs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted crossover&lt;/td&gt;
&lt;td&gt;~7 TB (our cost model, assumptions below)&lt;/td&gt;
&lt;td&gt;This article&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Backblaze B2: What You Get for $0.00695/GB
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Included?&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3-Compatible API&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;"S3 Compatible API" — mostly core operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;$0.00695/GB/month (~$6.95/TB/mo)&lt;/td&gt;
&lt;td&gt;~70% cheaper than AWS S3 Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uploads (Class A)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;No ingress fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downloads (Class B)&lt;/td&gt;
&lt;td&gt;Free up to 3× storage/mo, then $0.01/GB&lt;/td&gt;
&lt;td&gt;Generous egress, no per-GB surprise under 3×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimum commitment&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Pay for what you use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data durability&lt;/td&gt;
&lt;td&gt;11 nines claimed&lt;/td&gt;
&lt;td&gt;Standard for industry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presigned URLs&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Downloads and uploads; browser POST uploads not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side encryption&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;SSE-B2 (Backblaze-managed) or SSE-C (customer-managed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API transactions&lt;/td&gt;
&lt;td&gt;Free (Class A/B/C)&lt;/td&gt;
&lt;td&gt;Class D $0.004 per 10,000 calls; first 2,500/day free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multipart Upload&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle rules&lt;/td&gt;
&lt;td&gt;⚠️ Basic&lt;/td&gt;
&lt;td&gt;Hide/delete only, no tier transitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object Lock / WORM&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Governance + compliance retention, legal hold, bucket default retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-region replication&lt;/td&gt;
&lt;td&gt;⚠️ Cloud Replication&lt;/td&gt;
&lt;td&gt;Within or between regions, but a cross-region target needs a second account; Native API/console only, 2 rules per bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACLs / IAM roles / object tagging&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Officially listed as unsupported in the S3-Compatible API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Where B2 shines:&lt;/strong&gt; Backups, archives, media asset libraries, and disaster-recovery targets. These are workloads where you write once, read occasionally, and want a bill you can predict from one line item.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where B2 struggles:&lt;/strong&gt; Heavy egress above 3× your stored capacity, where the $0.01/GB overage applies. Also any workload that needs lifecycle tier transitions, object tagging, ACLs, or IAM roles. Backblaze lists each of those as unsupported in its S3-Compatible API, and B2 lifecycle rules only hide, delete, or cancel unfinished large uploads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Crossover Math: When Self-Hosted Wins
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Monthly Data Stored&lt;/th&gt;
&lt;th&gt;Backblaze B2&lt;/th&gt;
&lt;th&gt;Self-Hosted (RustFS)&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 TB&lt;/td&gt;
&lt;td&gt;$6.95&lt;/td&gt;
&lt;td&gt;~$38 (hardware amortized + ops)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;B2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 TB&lt;/td&gt;
&lt;td&gt;$69.50&lt;/td&gt;
&lt;td&gt;~$53&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RustFS (-24%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 TB&lt;/td&gt;
&lt;td&gt;$347.50&lt;/td&gt;
&lt;td&gt;~$142&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RustFS (-59%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 TB&lt;/td&gt;
&lt;td&gt;$695&lt;/td&gt;
&lt;td&gt;~$253&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RustFS (-64%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500 TB&lt;/td&gt;
&lt;td&gt;$3,475&lt;/td&gt;
&lt;td&gt;~$917&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RustFS (-74%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Assumptions: B2 egress within the free 3×-storage tier (so B2 cost ≈ storage cost). Self-hosted hardware amortized over 36 months. Ops cost = 0.15 FTE allocation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a typical backup or archive workload the crossover lands near 7 TB, inside the 5–10 TB band. Below it, B2's convenience usually wins on price and on not having to run anything. Above it, self-hosted pulls clearly ahead on cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Factor: Download Costs
&lt;/h2&gt;

&lt;p&gt;Backblaze changed its pricing model: egress is now free up to 3× your average monthly storage, and free beyond that through CDN or compute partners. Only egress above 3× storage is billed, at $0.01/GB. For most backup and archive workloads that makes download cost effectively zero, a real change from the old per-GB download charges.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Monthly Egress Cost (B2)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 TB stored, 10% egress/mo (100 GB)&lt;/td&gt;
&lt;td&gt;$0 (within 3× free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 TB stored, 25% egress/mo (2.5 TB)&lt;/td&gt;
&lt;td&gt;$0 (within 3× free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 TB stored, 50% egress/mo (25 TB)&lt;/td&gt;
&lt;td&gt;$0 (within 3× free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 TB stored, 400% egress/mo (400 TB)&lt;/td&gt;
&lt;td&gt;$1,000 (100 TB overage × $0.01)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Low-egress backup and archive workloads pay nothing for B2 downloads. Heavy egress (a CDN origin, ML training pulls, analytics above 3× storage per month) starts to cost. Self-hosted S3 stays at $0 either way, because the bandwidth is yours.&lt;/p&gt;

&lt;p&gt;Self-hosted S3: &lt;strong&gt;download cost = $0&lt;/strong&gt; (it's your network, your bandwidth).&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Framework
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Choose Backblaze B2 If:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ You store &lt;strong&gt;&amp;lt; 10TB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Primary workload is &lt;strong&gt;backup/archive&lt;/strong&gt; (write-once, read-rarely)&lt;/li&gt;
&lt;li&gt;✅ You want &lt;strong&gt;zero operational responsibility&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Download ratio is &lt;strong&gt;&amp;lt; 20%&lt;/strong&gt; of stored data monthly&lt;/li&gt;
&lt;li&gt;✅ You don't need lifecycle tier transitions, object tagging, ACLs, or IAM roles&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Choose Self-Hosted S3 (RustFS/MinIO) If:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ You store &lt;strong&gt;&amp;gt; 10TB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Workload is &lt;strong&gt;active&lt;/strong&gt; (frequent reads AND writes)&lt;/li&gt;
&lt;li&gt;✅ You want &lt;strong&gt;predictable $0 egress at any scale&lt;/strong&gt; (B2's free egress caps at 3× storage/mo)&lt;/li&gt;
&lt;li&gt;✅ You have &lt;strong&gt;infra capacity&lt;/strong&gt; or want to build it&lt;/li&gt;
&lt;li&gt;✅ You need &lt;strong&gt;S3 surface B2 doesn't expose&lt;/strong&gt; (object tagging, ACLs, IAM roles, lifecycle transitions)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Choose Both (Hybrid):
&lt;/h3&gt;

&lt;p&gt;A lot of teams keep B2 as their off-site cold target and run self-hosted S3 for hot, active data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write → Self-hosted S3 (hot, fast, zero egress)
       │
       └── Lifecycle copy → B2 (cold archive, off-site)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The S3 API sits on both sides, so the same tooling moves data either way. The two tiers just carry different cost profiles.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;B2 has the simplest cloud storage pricing&lt;/strong&gt; ($0.00695/GB store, free egress up to 3× storage, then $0.01/GB). Strong fit for backups and archives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted S3 becomes cheaper around 7TB&lt;/strong&gt; in our model, depending on download activity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B2 egress is now free up to 3× storage&lt;/strong&gt; — only heavy egress (&amp;gt;3×) costs; self-hosted is always $0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B2 does support Object Lock&lt;/strong&gt; (governance/compliance + legal hold) and bucket replication, but not object tagging, ACLs, IAM roles, or lifecycle tier transitions. Check your requirements against the list above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid is common:&lt;/strong&gt; self-hosted for hot data, B2 for cold archive. The shared S3 API makes the split workable.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt; — Apache 2.0, S3-compatible object storage you run yourself, with bucket replication, versioning, and event notifications available today (lifecycle management is still marked under testing). &lt;a href="https://rustfs.com/download" rel="noopener noreferrer"&gt;Download here&lt;/a&gt; or read the code on &lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Backblaze B2 really S3-compatible?
&lt;/h3&gt;

&lt;p&gt;Mostly. B2's S3-Compatible API covers the core operations (PutObject, GetObject, ListObjects, Multipart Upload, DeleteObject), so S3-aware tools like rclone, restic, and Cyberduck work against it unchanged. Presigned URLs work for both downloads and uploads, and server-side encryption is supported via SSE-B2 or SSE-C. Backblaze officially lists five gaps: ACLs, IAM roles, object tagging, website configuration, and browser-based POST uploads to presigned URLs. Cloud Replication is also absent from the S3 API. It runs through the Native API or the web console instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Backblaze B2 cheaper than AWS S3?
&lt;/h3&gt;

&lt;p&gt;On storage, clearly: $0.00695/GB against $0.023/GB for S3 Standard, about 70% less. Egress widens it further. B2 is free up to 3× your monthly storage and $0.01/GB above that, while AWS charges $0.09/GB after its free allowance. The catch is reach and feature surface. B2 runs 4 regions (US West, US East, EU Central, CA East) against AWS's 30-plus, and it has no storage classes to transition between. For backup and archive workloads, B2 ends up far below AWS S3 once egress is in the picture. In our cost model that gap runs to roughly 60–80%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Backblaze B2 have a free tier?
&lt;/h3&gt;

&lt;p&gt;Yes, and it doesn't expire. The first 10 GB of storage is always free, egress is free up to 3× your monthly storage, and Class A, B, and C transactions are free on pay-as-you-go. Class D calls cost $0.004 per 10,000, with the first 2,500 per day free. That is enough room to run a personal backup, a small-business archive, or a proof of concept without ever generating a bill, and it is more generous than most cloud storage free tiers.&lt;/p&gt;

&lt;h3&gt;
  
  
  When does self-hosted S3 storage become cheaper than Backblaze B2?
&lt;/h3&gt;

&lt;p&gt;Around 7 TB in our model, which amortizes hardware over 36 months and charges 0.15 FTE for operations. At 1 TB, B2 wins outright ($6.95 versus roughly $38 self-hosted). At 10 TB, self-hosted runs about 24% less; at 100 TB, about 64% less; at 500 TB, about 74% less. Egress shifts the line. B2's free 3× allowance covers most backup patterns, but a CDN origin or an ML training set pulling more than 3× storage per month pays $0.01/GB, while self-hosted egress stays at $0.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Backblaze B2 support Object Lock and cross-region replication?
&lt;/h3&gt;

&lt;p&gt;Both, with caveats. Object Lock is fully supported — governance and compliance retention modes, legal hold, and bucket-level default retention — and lifecycle rules will not delete a version that Object Lock protects. Replication runs through Backblaze Cloud Replication, which copies buckets within or between regions, but a cross-region target requires a second Backblaze account (an account is tied to one region), it allows only two rules per bucket, and it is driven by the Native API or web console rather than the S3 API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Every Backblaze figure and feature claim above was checked against Backblaze's own documentation on 2026-08-04:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Storage price, free egress, free tier, transaction classes — &lt;a href="https://www.backblaze.com/cloud-storage/pricing" rel="noopener noreferrer"&gt;Backblaze B2 pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Object Lock (governance/compliance modes, legal hold, default retention) — &lt;a href="https://www.backblaze.com/docs/cloud-storage-enable-object-lock-with-the-native-api" rel="noopener noreferrer"&gt;Backblaze Object Lock docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloud Replication and the cross-region second-account requirement — &lt;a href="https://www.backblaze.com/docs/cloud-storage-cloud-replication" rel="noopener noreferrer"&gt;Backblaze Cloud Replication docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lifecycle rule actions (hide, delete, cancel unfinished large files) — &lt;a href="https://www.backblaze.com/docs/cloud-storage-lifecycle-rules" rel="noopener noreferrer"&gt;Backblaze Lifecycle Rules docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;S3-Compatible API gaps (ACLs, IAM roles, object tagging, website config, POST uploads) — &lt;a href="https://www.backblaze.com/docs/cloud-storage-s3-compatible-api" rel="noopener noreferrer"&gt;Backblaze S3-Compatible API docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Region list — &lt;a href="https://www.backblaze.com/computer-backup/docs/data-centers-and-data-regions" rel="noopener noreferrer"&gt;Backblaze data centers and regions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RustFS license and feature status — &lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;RustFS on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS S3 list prices ($0.023/GB Standard, $0.09/GB egress) are AWS published pricing.&lt;/p&gt;

</description>
      <category>s3</category>
      <category>backblaze</category>
      <category>cost</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Multi-Cloud S3-Compatible Strategy: Avoiding Vendor Lock-In (2026)</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/multi-cloud-s3-compatible-strategy-avoiding-vendor-lock-in-2026-59bn</link>
      <guid>https://dev.to/ethan-carter/multi-cloud-s3-compatible-strategy-avoiding-vendor-lock-in-2026-59bn</guid>
      <description>&lt;h1&gt;
  
  
  Multi-Cloud S3-Compatible Strategy: Avoiding Vendor Lock-In (2026)
&lt;/h1&gt;

&lt;p&gt;Most teams assume they're portable because their app talks the S3 API. That assumption breaks the first time a cloud jacks up prices or you actually need to leave. The API was never the trap. Everything built on top of it is.&lt;/p&gt;

&lt;p&gt;This is a practical guide to keeping the option to move, cheaply, without re-architecting your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lock-In Problem: Where S3 Actually Traps You
&lt;/h2&gt;

&lt;p&gt;S3 compatibility cuts both ways. Every vendor implements the core API, so your PutObject and GetObject calls port in theory. In practice, "portable in theory" falls apart two layers deeper, at the parts most teams never think about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 is the API, and it's portable.&lt;/strong&gt; &lt;code&gt;put_object&lt;/code&gt; / &lt;code&gt;get_object&lt;/code&gt; work everywhere. No issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 is non-standard features.&lt;/strong&gt; Lifecycle rules, Object Lock modes, S3 Select, bucket logging. The syntax differs per vendor. What works on AWS does not drop into MinIO or RustFS cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 is ecosystem glue.&lt;/strong&gt; IAM, event routing (SQS/SNS/Lambda vs Pub/Sub vs Event Grid), monitoring (CloudWatch vs Stackdriver vs Azure Monitor), CI/CD wired to one CLI. None of it moves with your data.&lt;/p&gt;

&lt;p&gt;We've watched teams discover they were locked in at Layer 2 or 3, not Layer 1. The S3 API was fine. The forty other things they'd wired around it were not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Portable S3 Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Abstract the endpoint from day one
&lt;/h3&gt;

&lt;p&gt;Don't hardcode &lt;code&gt;s3.us-east-1.amazonaws.com&lt;/code&gt;. Every S3 SDK takes an &lt;code&gt;endpoint_url&lt;/code&gt;. Use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S3_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# https://s3.us-east-1.amazonaws.com or http://localhost:9000
&lt;/span&gt;    &lt;span class="n"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line is the difference between "we can move if we must" and "we're stuck."&lt;/p&gt;

&lt;h3&gt;
  
  
  Stick to core operations
&lt;/h3&gt;

&lt;p&gt;If portability matters, restrict yourself to what every serious S3 implementation actually supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Safe (universal)&lt;/th&gt;
&lt;th&gt;Caution (varies)&lt;/th&gt;
&lt;th&gt;Avoid (vendor-specific)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PutObject / GetObject&lt;/td&gt;
&lt;td&gt;Lifecycle rules&lt;/td&gt;
&lt;td&gt;Event notifications (SQS/Lambda)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ListObjectsV2&lt;/td&gt;
&lt;td&gt;Object Lock modes&lt;/td&gt;
&lt;td&gt;IAM-policy access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeleteObject&lt;/td&gt;
&lt;td&gt;S3 Select&lt;/td&gt;
&lt;td&gt;KMS-managed keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multipart Upload&lt;/td&gt;
&lt;td&gt;Bucket logging&lt;/td&gt;
&lt;td&gt;Requester-pays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HeadObject&lt;/td&gt;
&lt;td&gt;Presigned URL quirks&lt;/td&gt;
&lt;td&gt;Cross-account roles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CopyObject&lt;/td&gt;
&lt;td&gt;Replication config&lt;/td&gt;
&lt;td&gt;Glacier retrieval tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rule of thumb: if it isn't in the MinIO + RustFS + Ceph RGW docs, assume it won't move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Own your orchestration layer
&lt;/h3&gt;

&lt;p&gt;Stop reaching for cloud-native orchestration for anything S3-critical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Cloud-native (locked)&lt;/th&gt;
&lt;th&gt;Portable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workflows&lt;/td&gt;
&lt;td&gt;Step Functions / Logic Apps&lt;/td&gt;
&lt;td&gt;Airflow / Dagster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETL&lt;/td&gt;
&lt;td&gt;Glue DataBrew&lt;/td&gt;
&lt;td&gt;dbt / Spark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catalog&lt;/td&gt;
&lt;td&gt;Glue Catalog&lt;/td&gt;
&lt;td&gt;Iceberg (metadata lives in S3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduling&lt;/td&gt;
&lt;td&gt;EventBridge / CloudWatch Cron&lt;/td&gt;
&lt;td&gt;Airflow DAGs / cron&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;Secrets Manager / Key Vault&lt;/td&gt;
&lt;td&gt;Vault / env vars&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Test portability every quarter
&lt;/h3&gt;

&lt;p&gt;Don't wait for a forced migration to learn you weren't portable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stand up MinIO or &lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt; in Docker (under a minute).&lt;/li&gt;
&lt;li&gt;Point staging at it.&lt;/li&gt;
&lt;li&gt;Run your suite.&lt;/li&gt;
&lt;li&gt;Write down what broke.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Costs a couple of hours a quarter. Beats finding out during a six-month migration with a real egress bill attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Hosting: Maximum Portability
&lt;/h2&gt;

&lt;p&gt;The most portable setup is running your own S3 everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Region A (AWS us-east-1)   → RustFS on EC2
Region B (Azure westeurope) → RustFS on Azure VMs
Region C (GCP asia-east1)  → RustFS on GCE
On-prem (Tokyo)            → RustFS on bare metal
Edge (stores)              → RustFS on ARM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same binary, same API, same ops model. You kill the egress between your own regions, you control the upgrade cadence, and your data stays where you put it. The trade is that you run it. If you already operate K8s, Postgres, and app servers in each region, adding S3 is incremental. Not a new skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Multi-Cloud Is a Waste of Time
&lt;/h2&gt;

&lt;p&gt;Honest boundaries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Under 5TB&lt;/td&gt;
&lt;td&gt;Don't bother. Migration cost beats the savings.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Under 5 engineers&lt;/td&gt;
&lt;td&gt;Don't bother. Ops overhead beats the option value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy Lambda / SageMaker / BigQuery use&lt;/td&gt;
&lt;td&gt;Partial. Storage moves, compute doesn't.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulatory region lock&lt;/td&gt;
&lt;td&gt;Comply. Law beats preference.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early-stage startup&lt;/td&gt;
&lt;td&gt;Ship first. Revisit at Series B.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Lock-in lives at Layers 2 and 3 (features + ecosystem), not Layer 1 (the API).&lt;/li&gt;
&lt;li&gt;Abstract &lt;code&gt;endpoint_url&lt;/code&gt; from day one. Never hardcode a region endpoint.&lt;/li&gt;
&lt;li&gt;Use core S3 operations only. Test against MinIO/RustFS every quarter.&lt;/li&gt;
&lt;li&gt;Own your orchestration (Airflow/dbt/Iceberg beat Step Functions/Glue).&lt;/li&gt;
&lt;li&gt;Self-hosted S3 across regions is max portability. Same binary, different tin.&lt;/li&gt;
&lt;li&gt;Multi-cloud isn't free. Pay for it only when the option value beats the ops cost.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;RustFS runs identically on AWS, Azure, GCP, and bare metal. One binary, any infrastructure, Apache 2.0. &lt;a href="https://rustfs.com/download" rel="noopener noreferrer"&gt;Download&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is the S3 API actually portable between clouds?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Core operations (PutObject, GetObject, ListObjects, Multipart Upload) are genuinely portable. They behave identically across AWS S3, MinIO, RustFS, Ceph RGW, and Wasabi. But roughly a third of the full S3 surface is vendor extensions (event notifications, specialized encryption, IAM, request-payer config) that don't transfer. For apps using core operations only, portability is real. For apps wired into one cloud's ecosystem, it's an illusion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does multi-cloud storage cost versus single-cloud?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Self-hosted S3 (RustFS or MinIO) on cheap VMs across regions usually runs 40 to 70 percent under each cloud's native service. You drop per-GB premiums, cross-instance egress, and request fees, but you pick up ops overhead. Running multiple clouds' native S3 at once normally costs more than single-cloud, because of redundant storage plus cross-cloud egress. The cheapest pattern is self-hosted S3 on the cheapest VM in each region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the fastest way to test S3 portability?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stand up MinIO (&lt;code&gt;docker run -p 9000:9000 minio/minio server /data&lt;/code&gt;) or RustFS in Docker, point &lt;code&gt;S3_ENDPOINT&lt;/code&gt; at localhost:9000, and run your suite. If 95 percent plus passes with no code changes, you're portable. Document the failures, because those are your lock-in points. Do it quarterly. About two hours, versus weeks of firefighting and five-figure egress during a forced move.&lt;/p&gt;

</description>
      <category>s3</category>
      <category>multicloud</category>
      <category>vendorlockin</category>
      <category>rustfs</category>
    </item>
    <item>
      <title>S3 Metadata: Indexing Billions of Objects Without Falling Over</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Wed, 12 Aug 2026 15:29:51 +0000</pubDate>
      <link>https://dev.to/ethan-carter/s3-metadata-indexing-billions-of-objects-without-falling-over-15ob</link>
      <guid>https://dev.to/ethan-carter/s3-metadata-indexing-billions-of-objects-without-falling-over-15ob</guid>
      <description>&lt;h1&gt;
  
  
  S3 Metadata: Indexing Billions of Objects Without Falling Over
&lt;/h1&gt;

&lt;p&gt;If you operate S3-compatible storage at scale, slow ListObjectsV2 requests will eventually find you. Stay under your metadata threshold, and listings return fast. Cross it, and you'll watch timeouts, Slow Down errors, and after-hours alerts pile up.&lt;/p&gt;

&lt;p&gt;Every storage backend hits this wall at a different object count. Let's walk through what breaks, how popular systems behave, and practical fixes you can roll out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What even is S3 metadata?
&lt;/h2&gt;

&lt;p&gt;When you upload an object to S3, two pieces get saved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your raw file data&lt;/li&gt;
&lt;li&gt;Object metadata — all descriptive information attached to that file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what metadata typically includes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metadata Field&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Typical Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Object Key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;logs/app/2026-07-24/abc123.log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Variable (50–500 bytes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content Length&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1234567 bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application/json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~25 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETag&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"d41d8cd98f00b204e9800998ecf8427e"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;36 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last-Modified&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-07-24T15:30:00Z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~24 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage Class&lt;/td&gt;
&lt;td&gt;&lt;code&gt;STANDARD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~12 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Tags&lt;/td&gt;
&lt;td&gt;&lt;code&gt;env=production&amp;amp;team=backend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Up to 2KB (combined)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Metadata Headers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x-amz-meta-owner:uid-12345&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Up to 2KB (combined)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RgvpLpBZLXhPvWXNk6H_8sN1FnNtIqF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~40 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each object adds roughly 200–800 bytes of metadata overhead, stored separately from your actual data payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LIST operations get painfully slow at scale
&lt;/h2&gt;

&lt;p&gt;Every ListObjectsV2 call forces the storage service to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scan its metadata index for keys matching your prefix and delimiter&lt;/li&gt;
&lt;li&gt;Apply filters and limits&lt;/li&gt;
&lt;li&gt;Sort matching keys lexicographically&lt;/li&gt;
&lt;li&gt;Paginate results (max 1,000 entries per page + continuation token)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real-world performance you'll observe (typical for naive prefix scans; your mileage varies with key layout and access patterns):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Under 10k objects:&lt;/strong&gt; List calls finish in &amp;lt;100ms, no issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~1M objects:&lt;/strong&gt; Queries take 1–5s depending on how narrow your prefix is&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~100M objects:&lt;/strong&gt; Latency balloons to 10–60+ seconds, or requests timeout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1B+ objects:&lt;/strong&gt; Naive LIST calls aren't reliable anymore — you need a proper plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real bottleneck isn't your file data
&lt;/h2&gt;

&lt;p&gt;The pain point is the metadata index. Major S3 implementations handle metadata very differently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Metadata Design&lt;/th&gt;
&lt;th&gt;Scaling Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS S3&lt;/td&gt;
&lt;td&gt;Proprietary distributed index&lt;/td&gt;
&lt;td&gt;Supports billions of objects; LIST throttling kicks in at extreme scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MinIO&lt;/td&gt;
&lt;td&gt;Object metadata in &lt;code&gt;xl.meta&lt;/code&gt; alongside data, with an in-cluster &lt;code&gt;.metacache&lt;/code&gt; + background scanner for LIST&lt;/td&gt;
&lt;td&gt;Scales into the hundreds of millions with selective prefixes; LIST at extreme scale depends on the metacache staying warm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ceph RGW&lt;/td&gt;
&lt;td&gt;RADOS per-entry metadata&lt;/td&gt;
&lt;td&gt;Scales reasonably, but LIST linearly scans matching entries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RustFS&lt;/td&gt;
&lt;td&gt;Embedded RocksDB + custom sharded index&lt;/td&gt;
&lt;td&gt;Built for scale — subsecond LIST with selective prefixes at 100M+ objects (validated in RustFS internal benchmarks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SeaweedFS&lt;/td&gt;
&lt;td&gt;Filer + LevelDB/SQL backend&lt;/td&gt;
&lt;td&gt;Performance heavily depends on your database backend choice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Actionable strategies for high-object-count buckets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Strategy 1: Namespace partitioning (start here)
&lt;/h3&gt;

&lt;p&gt;The highest-impact change you can make: don't cram everything into one bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Bad: One giant monolithic bucket
s3://prod-data/
├── logs/                    # 500M objects
├── user-uploads/            # 200M objects
├── ml-checkpoints/          # 50M objects
├── backups/                 # 10M objects
└── temp/                    # 5M objects
# Total: 765M objects. Every LIST scans every namespace.

# Better: Split workloads into dedicated buckets
s3://prod-logs/              # 500M objects (isolated metadata index)
s3://prod-uploads/           # 200M objects (isolated metadata index)
s3://prod-ml-checkpoints/    # 50M objects (isolated metadata index)
s3://prod-backups/           # 10M objects (isolated metadata index)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each bucket runs its own independent metadata index. Listing inside &lt;code&gt;prod-uploads&lt;/code&gt; never touches metadata from your log bucket.&lt;/p&gt;

&lt;p&gt;Quick note on cost: AWS doesn't charge per bucket, and self-hosted S3 systems generally don't penalize extra buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy 2: Structure your object prefixes smartly
&lt;/h3&gt;

&lt;p&gt;Even within a single bucket, organized key paths drastically speed up prefix filtering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Poor: Flat layout — LIST must scan everything
s3://bucket/object-000001.json
s3://bucket/object-000002.json

# Improved: Hierarchical prefixes narrow the scan range
s3://bucket/year=2026/month=07/day=24/hour=15/object-abc123.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request using &lt;code&gt;Prefix="year=2026/month=07/"&lt;/code&gt; only loads metadata for July data, instead of your entire bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy 3: Avoid full-bucket scans at all costs
&lt;/h3&gt;

&lt;p&gt;This anti-pattern destroys performance on large buckets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ Don't use this against large buckets — scans full metadata
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_objects_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-bucket&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ✅ Use targeted prefix pagination
&lt;/span&gt;&lt;span class="n"&gt;paginator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_paginator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;list_objects_v2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paginator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-bucket&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;logs/2026-07/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Contents&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Strategy 4: Run an external metadata index
&lt;/h3&gt;

&lt;p&gt;Once you hit roughly 100M objects, maintain a separate metadata database alongside S3.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Sample schema for PostgreSQL / MySQL / SQLite external index&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;s3_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;size&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_modified&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Indexes tuned for common queries&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_s3_objects_bucket_prefix&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s3_objects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="n"&gt;text_pattern_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_s3_objects_tags&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s3_objects&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sync the table on every &lt;code&gt;PutObject&lt;/code&gt; / &lt;code&gt;DeleteObject&lt;/code&gt; via S3 event notifications or application hooks. Query metadata directly from the database, and only use S3 API to read/write actual files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff:&lt;/strong&gt; You operate another database. &lt;strong&gt;Win:&lt;/strong&gt; metadata lookup latency stays consistent no matter how many objects you store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy 5: Pick storage built for your expected scale
&lt;/h3&gt;

&lt;p&gt;If you know you'll grow past 100M objects, evaluate systems based on their metadata architecture.&lt;/p&gt;

&lt;p&gt;Good questions to ask vendors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What's p99 ListObjectsV2 latency on a 100M-object bucket with a selective prefix?&lt;/li&gt;
&lt;li&gt;Does metadata run in-process, or rely on an external coordination service?&lt;/li&gt;
&lt;li&gt;How does LIST performance hold up during rebalancing or recovery?&lt;/li&gt;
&lt;li&gt;Can I read metadata without going through the standard S3 API?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;RustFS metadata overview:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every node hosts embedded RocksDB for metadata. Clusters shard metadata by object key hash:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single node:&lt;/strong&gt; Subsecond LIST responses for 100M+ objects with selective prefixes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clustered:&lt;/strong&gt; Metadata split evenly across nodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery:&lt;/strong&gt; Metadata replicates alongside object data; LIST stays available (minor staleness possible)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline option:&lt;/strong&gt; Export metadata snapshots to SQLite for analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When native S3 metadata stops fitting your workload
&lt;/h2&gt;

&lt;p&gt;Once you hit ~1B objects and need complex filtering, vanilla object storage isn't ideal for metadata-heavy workflows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Constant LIST timeouts&lt;/td&gt;
&lt;td&gt;Deploy external metadata index (Strategy 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex filters (size limits, tag matching)&lt;/td&gt;
&lt;td&gt;Data lake formats (Iceberg / Delta Lake) or dedicated metadata DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-text search over object names&lt;/td&gt;
&lt;td&gt;Add Elasticsearch / OpenSearch sidecar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relational-style joins on object metadata&lt;/td&gt;
&lt;td&gt;Move metadata out of S3 into a relational database&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;S3 shines when you fetch objects by known keys. It was never designed as a flexible query engine for object metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every S3-compatible storage hits a metadata scaling limit. Find your threshold before production hits it.&lt;/li&gt;
&lt;li&gt;Split data across multiple buckets — this gives you the biggest performance return.&lt;/li&gt;
&lt;li&gt;Build hierarchical object keys to make prefix filtering effective.&lt;/li&gt;
&lt;li&gt;Never run full-bucket scans in production; always use prefix filters + pagination.&lt;/li&gt;
&lt;li&gt;Around 100M objects, plan an event-driven external metadata store (PostgreSQL or SQLite).&lt;/li&gt;
&lt;li&gt;If you expect massive scale, prioritize metadata architecture during storage selection. RustFS (RocksDB-backed) and AWS S3 handle billions of objects better than etcd-dependent stacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tired of slow LIST calls with millions of stored objects? RustFS uses RocksDB-backed metadata to deliver subsecond listings even at 100M+ objects. &lt;a href="https://rustfs.com/download" rel="noopener noreferrer"&gt;Download RustFS&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is there a hard maximum number of objects per bucket?
&lt;/h3&gt;

&lt;p&gt;Theoretically no. AWS says buckets support virtually unlimited objects. Practically, your limit comes from metadata index speed.&lt;/p&gt;

&lt;p&gt;Most teams notice LIST slowdowns between 10M–50M objects, depending on key layout and access patterns. With clean partitioning and prefix design, 100M+ objects work fine. Once you near 1B objects, build an external metadata index regardless of storage platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do ListObjects requests slow down as object counts grow?
&lt;/h3&gt;

&lt;p&gt;ListObjects scans matching metadata entries, sorts keys lexicographically, and paginates results. Sorting creates heavy O(N log N) overhead when thousands or millions of entries match your prefix.&lt;/p&gt;

&lt;p&gt;Worse: most clients loop pagination, turning one logical job into dozens of API round-trips stacked with network latency.&lt;/p&gt;

&lt;p&gt;Fixes: tighter prefix filters, bucket partitioning, or offloading metadata queries to an external database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does bucket naming affect S3 performance?
&lt;/h3&gt;

&lt;p&gt;Generally not. Bucket names are routing and namespace identifiers, not performance controls. Operationally, stuffing all data into a small number of giant buckets concentrates metadata pressure and slows LIST. Spreading load across more buckets distributes pressure. AWS explicitly confirms bucket naming does not impact performance.&lt;/p&gt;

&lt;p&gt;The same applies to self-hosted systems like MinIO and RustFS. Bucket-level metadata overhead is negligible. Spend tuning time on key structure and partitioning instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the standard way to track S3 metadata externally?
&lt;/h3&gt;

&lt;p&gt;The common pattern: PostgreSQL (or MySQL) with &lt;code&gt;(bucket, object_key)&lt;/code&gt; as the primary key.&lt;/p&gt;

&lt;p&gt;Sync updates via S3 event notifications (SQS/SNS/Lambda on AWS; webhooks for self-hosted storage) on object creates, copies, and deletes. Query metadata directly from the database and reserve S3 API purely for file reads and writes.&lt;/p&gt;

&lt;p&gt;For lighter workloads with tens of millions of rows, SQLite works well. If you need search, pair the relational database with Elasticsearch or OpenSearch. The downside is another system to maintain; the upside is stable metadata query performance independent of bucket size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I replace LIST with S3 Select for metadata filtering?
&lt;/h3&gt;

&lt;p&gt;No. S3 Select runs SQL inside object payloads (CSV, JSON, Parquet). It cannot filter on object-level attributes: names, sizes, tags, timestamps.&lt;/p&gt;

&lt;p&gt;Metadata filtering still requires either ListObjectsV2 (with scaling limits) or an external index. S3 Select solves a separate problem: scanning file contents without full downloads. Both tools are useful, but they cannot replace each other.&lt;/p&gt;

</description>
      <category>s3</category>
      <category>metadata</category>
      <category>objectstorage</category>
      <category>scalability</category>
    </item>
    <item>
      <title>Distributed Storage 101: How It Works and When You Actually Need It</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Sun, 09 Aug 2026 11:39:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/distributed-storage-101-how-it-works-and-when-you-actually-need-it-2a19</link>
      <guid>https://dev.to/ethan-carter/distributed-storage-101-how-it-works-and-when-you-actually-need-it-2a19</guid>
      <description>&lt;p&gt;Distributed storage isn't magic — it's a set of trade-offs. Here's how it actually works under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Stats&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Figure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Orgs running distributed storage (mid-size+)&lt;/td&gt;
&lt;td&gt;~68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary driver: HA/failure tolerance&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-node disk failure recovery time&lt;/td&gt;
&lt;td&gt;2–8 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teams that regretted distributing too early&lt;/td&gt;
&lt;td&gt;~23%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Makes Storage "Distributed"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Single-node:&lt;/strong&gt; One process, one machine. If it dies → outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distributed:&lt;/strong&gt; Data spread across multiple nodes. Individual failures don't cause data loss or downtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Single Node:   App → [Server + Disk] → if dies → GONE
Distributed:   App → [Node A] [Node B] [Node C] → if A dies → B+C serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Three Problems It Solves
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Availability
&lt;/h3&gt;

&lt;p&gt;Hardware fails. Distributed systems survive individual component failures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure Type&lt;/th&gt;
&lt;th&gt;Single-Node&lt;/th&gt;
&lt;th&gt;Distributed (3+ nodes)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Disk crash&lt;/td&gt;
&lt;td&gt;Outage&lt;/td&gt;
&lt;td&gt;Auto-rebalance, zero downtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Machine crash&lt;/td&gt;
&lt;td&gt;Outage&lt;/td&gt;
&lt;td&gt;Other nodes take over&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rack power loss&lt;/td&gt;
&lt;td&gt;Outage&lt;/td&gt;
&lt;td&gt;If cross-rack → degraded but alive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; &amp;gt;99.9% uptime SLA? Distributed gets you there by design.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Scale Beyond One Box
&lt;/h3&gt;

&lt;p&gt;One machine has limits (~500TB raw in a big chassis). Need 1 PB+? Add nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Geographic Distribution
&lt;/h3&gt;

&lt;p&gt;Multi-region for compliance, latency, DR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;US-East (Nodes 1-4)  ← primary
EU-West (Nodes 5-7)   ← compliance + EU users
APAC   (Nodes 8-10)   ← Asia + DR target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Data Gets Placed: Consistent Hashing
&lt;/h2&gt;

&lt;p&gt;Most systems (RustFS, Ceph, Cassandra) use consistent hashing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hash the object key → big integer&lt;/li&gt;
&lt;li&gt;Map to position on a hash ring (circle of values)&lt;/li&gt;
&lt;li&gt;Each node owns a range of the ring&lt;/li&gt;
&lt;li&gt;Object stored on node(s) whose range contains its hash&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why not &lt;code&gt;hash % num_nodes&lt;/code&gt;?&lt;/strong&gt; Adding/removing a node with consistent hashing moves only ~1/N of data. Mod-N reshuffles everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replication vs. Erasure Coding
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Replication (Simple, Space-Heavy)
&lt;/h3&gt;

&lt;p&gt;Store N complete copies on different nodes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3× replication:&lt;/strong&gt; 200% space overhead, tolerate 2 failures, fast reads&lt;/li&gt;
&lt;li&gt;Best for: hot data, frequent reads&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Erasure Coding (Complex, Space-Efficient)
&lt;/h3&gt;

&lt;p&gt;Split data into fragments + compute parity.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4+2 scheme:&lt;/strong&gt; 50% overhead, tolerate 2 failures, slower recovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8+3 scheme:&lt;/strong&gt; 37.5% overhead, tolerate 3 failures&lt;/li&gt;
&lt;li&gt;Best for: warm/cold data, capacity-sensitive workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;3× Replication&lt;/th&gt;
&lt;th&gt;4+2 EC&lt;/th&gt;
&lt;th&gt;8+3 EC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Space overhead&lt;/td&gt;
&lt;td&gt;200%&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;37.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failures tolerated&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read speed&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write speed&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Slower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most production systems use both: replicate hot data, erasure-code cold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consistency Models
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Strong Consistency (CP)
&lt;/h3&gt;

&lt;p&gt;Every read returns the most recent write. Always.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; Higher latency during writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Used by:&lt;/strong&gt; RustFS (default), Ceph, financial systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Eventual Consistency (AP)
&lt;/h3&gt;

&lt;p&gt;Writes acknowledged immediately; reads &lt;em&gt;might&lt;/em&gt; be briefly stale.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Benefit:&lt;/strong&gt; Lower write latency, higher availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Used by:&lt;/strong&gt; S3 (cross-region), Cassandra, DynamoDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceptable for:&lt;/strong&gt; Content delivery, analytics, logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Practical reality:&lt;/strong&gt; Most systems are strong-consistency within a DC, eventual across regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  CAP Theorem Explained
&lt;/h2&gt;

&lt;p&gt;CAP = Consistency + Availability + Partition tolerance. Pick two.&lt;/p&gt;

&lt;p&gt;But P is &lt;strong&gt;not optional&lt;/strong&gt; — networks partition. Real choice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CP&lt;/th&gt;
&lt;th&gt;AP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;During network split&lt;/td&gt;
&lt;td&gt;Stop writes to avoid divergence&lt;/td&gt;
&lt;td&gt;Accept writes, reconcile later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data safety&lt;/td&gt;
&lt;td&gt;✅ No corruption&lt;/td&gt;
&lt;td&gt;⚠️ Possible conflicts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime&lt;/td&gt;
&lt;td&gt;⚠️ Partially unavailable&lt;/td&gt;
&lt;td&gt;✅ Fully available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;RustFS, Ceph, MongoDB&lt;/td&gt;
&lt;td&gt;Cassandra, DynamoDB, S3 x-repl&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Most object storage is CP&lt;/strong&gt; — divergent data is worse than brief unavailability.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You DON'T Need Distributed Storage
&lt;/h2&gt;

&lt;p&gt;Real costs to consider:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;td&gt;Monitoring 3–15 nodes vs 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;Network round-trips vs local disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overhead&lt;/td&gt;
&lt;td&gt;Metadata services consume CPU/RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;3–6 months to proficiency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging&lt;/td&gt;
&lt;td&gt;More moving parts = harder troubleshooting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Stay single-node when:&lt;/strong&gt; &amp;lt;10TB, downtime acceptable, no ops team, dev/homelab, budget tight.&lt;/p&gt;

&lt;p&gt;RustFS, MinIO, SeaweedFS all run great as single-node. Distribute when you hit a concrete wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Distributed Storage Breaks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Split-brain:&lt;/strong&gt; Both sides of a partition accept writes → divergent data. CP systems refuse minority-side writes; AP systems hope for the best.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebalancing storms:&lt;/strong&gt; Adding a node triggers data migration that can degrade performance. Schedule during low traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow node problem:&lt;/strong&gt; One slow node gets more retries → more load → slower. Thundering herd cascade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-node failure:&lt;/strong&gt; Losing 3 nodes simultaneously (rack failure) may require human intervention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring complexity:&lt;/strong&gt; "Is the cluster healthy?" has 6+ dimensions, not a yes/no answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  From Single Node to Cluster: Progressive Phases
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Phase 1: Single Node &lt;span class="o"&gt;(&lt;/span&gt;now&lt;span class="o"&gt;)&lt;/span&gt; — official &lt;span class="nb"&gt;command&lt;/span&gt;, sourced verbatim from RustFS GitHub README &lt;span class="o"&gt;[&lt;/span&gt;NOT EXECUTED IN CI]
  docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 9000:9000 &lt;span class="nt"&gt;-p&lt;/span&gt; 9001:9001 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/logs:/logs rustfs/rustfs:latest

Phase 2: Replicated Cluster &lt;span class="o"&gt;(&lt;/span&gt;need HA&lt;span class="o"&gt;)&lt;/span&gt;
  Add &lt;span class="nt"&gt;--cluster-nodes&lt;/span&gt; flag, deploy on 3 machines

Phase 3: Erasure-Coded Cluster &lt;span class="o"&gt;(&lt;/span&gt;need efficiency&lt;span class="o"&gt;)&lt;/span&gt;
  Configure ec.scheme&lt;span class="o"&gt;=&lt;/span&gt;4+2 &lt;span class="o"&gt;(&lt;/span&gt;needs 6+ drives&lt;span class="o"&gt;)&lt;/span&gt;

Phase 4: Multi-Region &lt;span class="o"&gt;(&lt;/span&gt;need geo-redundancy&lt;span class="o"&gt;)&lt;/span&gt;
  Add remote nodes, configure async replication rules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't skip phases. Teams jumping from Phase 1→4 usually spend the next quarter debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Distributed storage solves real problems at the cost of real complexity. The right move for most teams: &lt;strong&gt;start single-node, distribute when you hit a concrete wall.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RustFS runs as a trivially simple single-node deploy and scales to clustered erasure-coded with the same binary — making it a strong "start here, stay here" option for teams leaving MinIO or outgrowing standalone setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between distributed storage and just putting files on a server?&lt;/strong&gt;&lt;br&gt;
Single-server storage has a single point of failure. If that disk dies or that machine loses power, your data is gone until you restore from backup. Distributed storage spreads data across multiple nodes so individual failures don't cause data loss or downtime. The trade-off is operational complexity — you're now managing a cluster instead of a filesystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should I use distributed storage vs. single-node?&lt;/strong&gt;&lt;br&gt;
Use single-node (RustFS standalone, MinIO single-drive, SeaweedFS) for &amp;lt;10TB, non-critical workloads, homelabs, or development environments. Add distribution when you need high availability, capacity beyond one machine, throughput beyond one NIC/disk, or geographic redundancy. Don't distribute for distribution's sake — it adds latency, complexity, and operational overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does erasure coding work in simple terms?&lt;/strong&gt;&lt;br&gt;
Erasure coding splits data into N fragments, computes M parity fragments, and stores them across different drives/nodes. You can lose any M fragments and still reconstruct the data. A 4+2 scheme stores 4 data + 2 parity chunks across 6 locations; you lose any 2 and recover. Compared to 3x replication, 4+2 uses only 1.5x raw space while tolerating the same number of failures. The cost: reconstruction is CPU-intensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's CAP theorem and why does everyone argue about it?&lt;/strong&gt;&lt;br&gt;
CAP says a distributed system can guarantee at most two of three: Consistency, Availability, Partition tolerance. "P" isn't optional — networks do partition — so the real choice is CP (consistency over availability during a split) or AP (availability, accept temporary inconsistency). Most storage systems are CP because divergent data is worse than briefly unavailable data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is RustFS distributed? How does it compare to Ceph?&lt;/strong&gt;&lt;br&gt;
RustFS supports both single-node and clustered deployment. In clustered mode it uses erasure coding with configurable schemes (default 4+2) and supports adding/removing nodes dynamically. Ceph is more mature for massive deployments (100+ nodes, petabyte-scale) with unified object/block/file storage via RADOS. RustFS is simpler to operate, has lower resource overhead (95MB idle vs Ceph's GB-range), and is written in Rust which eliminates a class of memory-safety bugs. For most teams under 50PB, RustFS hits the complexity/performance sweet spot.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Feedback? &lt;a href="https://github.com/rustfs/rustfs/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>storage</category>
      <category>distributed</category>
      <category>ceph</category>
      <category>rustfs</category>
    </item>
    <item>
      <title>What Is Amazon S3, Really? (2026)</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:53:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/what-is-amazon-s3-really-2026-4cle</link>
      <guid>https://dev.to/ethan-carter/what-is-amazon-s3-really-2026-4cle</guid>
      <description>&lt;h1&gt;
  
  
  What Is Amazon S3, Really? The 2026 Explanation
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;S3 launched in March 2006 — before the iPhone, before Airbnb, before "the cloud" was a household term.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One Paragraph
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;S3 = flat key-value object store over HTTP.&lt;/strong&gt; You give it data + a key (&lt;code&gt;uploads/photo.jpg&lt;/code&gt;), it stores it redundantly. You request the key, it returns the data. Everything else — storage classes, encryption, replication — builds on that simple contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 vs Everything Else
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;vs File Systems&lt;/th&gt;
&lt;th&gt;vs Block Storage&lt;/th&gt;
&lt;th&gt;vs Databases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flat namespace (no real folders)&lt;/td&gt;
&lt;td&gt;API, not raw disk&lt;/td&gt;
&lt;td&gt;Opaque blobs, not structured records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eventually consistent for overwrites&lt;/td&gt;
&lt;td&gt;No seekable random access&lt;/td&gt;
&lt;td&gt;No query language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immutable objects&lt;/td&gt;
&lt;td&gt;Whole-object read/write&lt;/td&gt;
&lt;td&gt;Key-based access only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Pricing: 4 Components
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; $0.023/GB/mo (Standard)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requests:&lt;/strong&gt; $0.005/1K PUTs, $0.0004/1K GETs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egress:&lt;/strong&gt; $0.09/GB after 100GB free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data management:&lt;/strong&gt; Tags, analytics, replication&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A busy 10TB bucket: ~&lt;strong&gt;$530/month&lt;/strong&gt; (storage + requests + egress).&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage Class Ladder
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;$0.023/GB&lt;/td&gt;
&lt;td&gt;Hot data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IA&lt;/td&gt;
&lt;td&gt;$0.0125/GB&lt;/td&gt;
&lt;td&gt;Monthly access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glacier IR&lt;/td&gt;
&lt;td&gt;$0.004/GB&lt;/td&gt;
&lt;td&gt;Rare but fast retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deep Archive&lt;/td&gt;
&lt;td&gt;$0.00099/GB&lt;/td&gt;
&lt;td&gt;"Delete never"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Self-Hosted Alternatives
&lt;/h2&gt;

&lt;p&gt;MinIO, &lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt;, Ceph RGW all implement S3 API. Same &lt;code&gt;s3://&lt;/code&gt; URIs, zero egress/request fees on your own hardware.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does S3 stand for?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Simple Storage Service. Despite the name, there's nothing particularly "simple" about the full feature set anymore — it started simple in 2006 and accumulated capabilities (storage classes, replication, encryption, analytics) over 20 years. But the core contract remains elegantly simple: give me bytes + a key, I'll store them. Give me the key back, I'll return the bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is S3 a file system?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. S3 is an object store with a flat key namespace. The &lt;code&gt;/&lt;/code&gt; characters in keys look like directory separators but aren't — S3 has no real folders, no hierarchical structure, and no POSIX semantics (no hard links, symlinks, permissions, or in-place modification). Tools like &lt;code&gt;aws s3 sync&lt;/code&gt; and s3fs present a filesystem-like interface on top of S3, but underneath it's still a key-value store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does S3 really cost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on your usage pattern, but a typical production workload has 4 cost components: (1) Storage: $0.023/GB/month for Standard, (2) Requests: $0.005 per 1,000 PUTs, $0.0004 per 1,000 GETs, (3) Egress: $0.09/GB after the first 100GB free tier, (4) Data management: tags, analytics, replication. For a 10TB bucket with moderate activity (1M GETs/day, 2TB egress/month), expect $230 storage + $120 requests + $180 egress = ~$530/month. Self-hosted S3 alternatives eliminate components 2-4 entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run my own S3-compatible server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Multiple open-source projects implement the S3 API: MinIO (Go, largest community), RustFS (Rust, focused on performance), Ceph RGW (C++, part of Ceph platform), SeaweedFS (Go, simpler architecture). All support the core S3 operations (PutObject, GetObject, ListObjects, Multipart Upload) and can be dropped in as replacements for AWS S3 in most applications by changing the endpoint URL. See our bare metal deployment guide for details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the maximum file size in S3?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;5 TB per object. For files larger than 5 GB, you must use Multipart Upload, which splits the file into parts (5 MB to 5 GB each, up to 10,000 parts). This is handled automatically by AWS SDKs, the AWS CLI, and most S3 clients — your application code typically doesn't need to change. The 5 TB single-object limit has existed since S3's launch and hasn't changed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://rustfs.com" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt; — S3-compatible storage without the AWS bill. &lt;a href="https://rustfs.com/download" rel="noopener noreferrer"&gt;Download&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>storage</category>
      <category>explained</category>
    </item>
    <item>
      <title>FINRA-Compliant Storage with S3 Object Lock: A Practical Guide for 2026</title>
      <dc:creator>Ethan Carter</dc:creator>
      <pubDate>Fri, 07 Aug 2026 11:26:00 +0000</pubDate>
      <link>https://dev.to/ethan-carter/finra-compliant-storage-with-s3-object-lock-a-practical-guide-for-2026-53m1</link>
      <guid>https://dev.to/ethan-carter/finra-compliant-storage-with-s3-object-lock-a-practical-guide-for-2026-53m1</guid>
      <description>&lt;p&gt;FINRA Rule 17a-4(f) requires WORM storage. Here's how S3 Object Lock satisfies it on self-hosted infrastructure — with implementation examples and a comparison of which systems actually support compliance-mode locking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Stats&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Figure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FINRA enforcement actions citing recordkeeping (2025)&lt;/td&gt;
&lt;td&gt;~340&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg fine per firm for recordkeeping deficiencies&lt;/td&gt;
&lt;td&gt;$225K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firms using cloud WORM storage for 17a-4(f)&lt;/td&gt;
&lt;td&gt;~73%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What FINRA Actually Requires
&lt;/h2&gt;

&lt;p&gt;Three rules matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEC Rule 17a-4(f)&lt;/strong&gt; — Electronic records must be stored in either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Option A (WORM):&lt;/strong&gt; Non-rewriteable, non-erasable format — no one can modify or delete records until retention expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option B (Audit Trail):&lt;/strong&gt; Complete time-stamped audit trail of every modification/deletion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Option A is easier to defend in exams because the technology prevents tampering. Option A's digital standard: &lt;strong&gt;S3 Object Lock in Compliance mode.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FINRA Rule 4511(c)&lt;/strong&gt; — Records must be indexed, accessible (first 2 years immediately), and reproducible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FINRA Rule 3110&lt;/strong&gt; — Supervisory controls preventing unauthorized modification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compliance Mode vs. Governance Mode
&lt;/h2&gt;

&lt;p&gt;This is the critical distinction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compliance Mode:   locked → NO ONE can delete → ✅ FINRA OK
Governance Mode:  locked → root CAN bypass    → ⚠️ Need extra controls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For FINRA examination purposes, &lt;strong&gt;Compliance mode is the standard answer.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Systems Support Object Lock
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Object Lock&lt;/th&gt;
&lt;th&gt;Compliance Mode&lt;/th&gt;
&lt;th&gt;Legal Hold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS S3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RustFS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ (v1.2+)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MinIO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ceph RGW&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wasabi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SeaweedFS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Garage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SeaweedFS and Garage don't implement Object Lock at all. If you're using them for regulated data, you'll need an external WORM layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation: Step by Step
&lt;/h2&gt;

&lt;p&gt;Using RustFS as the S3-compatible backend (same pattern applies to any compliant system):&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Enable Object Lock at Bucket Creation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; http://localhost:9000 s3api create-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; finra-compliance-records &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--object-lock-enabled-for-bucket&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Object Lock must be enabled at creation — can't be added to existing buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Set Default Retention (6 years)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; http://localhost:9000 s3api put-object-lock-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; finra-compliance-records &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--object-lock-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
    "ObjectLockEnabled": "Enabled",
    "Rule": {
      "DefaultRetention": {
        "Mode": "COMPLIANCE",
        "Days": 2200
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Upload with Retention
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9000&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-secret&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Trade record: 6-year retention
&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;finra-compliance-records&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;trades/2026/07/T123456.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;T123456.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;ObjectLockMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;COMPLIANCE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ObjectLockRetainUntilDate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2032&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Verify Immutability (Critical!)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Try deleting a locked object — should FAIL with 403 AccessDenied&lt;/span&gt;
aws &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; http://localhost:9000 s3api delete-object &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; finra-compliance-records &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; trades/2026/07/T123456.json

&lt;span class="c"&gt;# Error: "The object is locked with COMPLIANCE mode retention..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Document this test.&lt;/strong&gt; Screenshot the error. Keep it for examiners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retention Strategy by Record Type
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record Type&lt;/th&gt;
&lt;th&gt;Retention&lt;/th&gt;
&lt;th&gt;Days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trade confirmations &amp;amp; order tickets&lt;/td&gt;
&lt;td&gt;6 years&lt;/td&gt;
&lt;td&gt;2200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer account records&lt;/td&gt;
&lt;td&gt;6 yrs post-closure&lt;/td&gt;
&lt;td&gt;2300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer communications&lt;/td&gt;
&lt;td&gt;3 years&lt;/td&gt;
&lt;td&gt;1100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blotters &amp;amp; ledgers&lt;/td&gt;
&lt;td&gt;6 years&lt;/td&gt;
&lt;td&gt;2200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complaint records&lt;/td&gt;
&lt;td&gt;4+ years&lt;/td&gt;
&lt;td&gt;1500+legal hold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing materials (RIAs)&lt;/td&gt;
&lt;td&gt;5 years&lt;/td&gt;
&lt;td&gt;1850&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Create separate buckets per retention tier — simplifies auditing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Hosted vs. Cloud for FINRA Workloads
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Self-Hosted (RustFS)&lt;/th&gt;
&lt;th&gt;Cloud (AWS/Wasabi)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WORM capability&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examiner familiarity&lt;/td&gt;
&lt;td&gt;⚠️ Needs explanation&lt;/td&gt;
&lt;td&gt;✅ Ends questions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost at 100TB+&lt;/td&gt;
&lt;td&gt;✅ 3–5x cheaper&lt;/td&gt;
&lt;td&gt;💰 Egress adds up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data sovereignty&lt;/td&gt;
&lt;td&gt;✅ You control it&lt;/td&gt;
&lt;td&gt;⚠️ Region-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D3P ecosystem&lt;/td&gt;
&lt;td&gt;Less common&lt;/td&gt;
&lt;td&gt;Well-established&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Self-hosting wins&lt;/strong&gt; when you have 50TB+, existing ops capacity, or data residency needs. &lt;strong&gt;Cloud wins&lt;/strong&gt; for smaller firms prioritizing examiner familiarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Self-Hosted Gets Tricky
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;D3P designation&lt;/strong&gt; is harder with self-hosted infra (cloud has established ecosystems)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examiner skepticism&lt;/strong&gt; — be ready with documentation, test results, config screenshots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version upgrades&lt;/strong&gt; need regression tests against WORM guarantees&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DR replica&lt;/strong&gt; must also enforce Object Lock (compliance gap if it doesn't)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None are deal-breakers — just budget engineering time accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-Exam Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Object Lock enabled on all compliance buckets (at creation)&lt;/li&gt;
&lt;li&gt;[ ] Default retention = &lt;strong&gt;Compliance&lt;/strong&gt; mode (not Governance)&lt;/li&gt;
&lt;li&gt;[ ] Retention periods match record-type requirements&lt;/li&gt;
&lt;li&gt;[ ] Legal hold mechanism tested and documented&lt;/li&gt;
&lt;li&gt;[ ] Failed delete attempts documented (screenshots!)&lt;/li&gt;
&lt;li&gt;[ ] DR replica also enforces Object Lock&lt;/li&gt;
&lt;li&gt;[ ] WSPs reference storage architecture&lt;/li&gt;
&lt;li&gt;[ ] D3P designation filed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;FINRA doesn't care if your WORM storage runs on AWS or your own RustFS cluster. What matters: &lt;strong&gt;can you prove tamper-proof retention, produce records fast, and demonstrate it to an examiner?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;S3 Object Lock in Compliance mode is the clearest path because the technology makes violations impossible — not just against policy. For self-hosted teams, &lt;strong&gt;RustFS&lt;/strong&gt; offers full Object Lock parity with AWS S3. Verify your version, test adversarially, document everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does FINRA require cloud storage, or can I self-host?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FINRA does not mandate cloud storage. What it mandates (via SEC Rule 17a-4(f)) is that electronic records be stored in a non-rewriteable, non-erasable format — WORM — for the applicable retention period. If your self-hosted S3-compatible system implements Object Lock in Compliance mode with tamper-proof retention, it can satisfy the WORM requirement. The key is whether your storage system's immutability controls meet the regulatory standard, not who hosts the servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between Governance mode and Compliance mode for Object Lock?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Governance mode allows users with special IAM permissions (like &lt;code&gt;s3:BypassGovernanceRetention&lt;/code&gt;) to overwrite or delete an object before retention expires. Compliance mode does not — no user, not even root or account admin, can delete or overwrite a locked object until retention expires. For FINRA Rule 17a-4(f) compliance, you need Compliance mode. Governance mode is useful for internal data governance but doesn't satisfy regulatory WORM requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long do I need to retain records under FINRA rules?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on the record type. Most customer communications and trade records require 3–6 years. Customer account records need 6 years after account closure. Blotters and ledgers typically require 6 years. Complaint records need at least 4 years (often longer if litigation risk exists). The first 2 years must be "easily accessible" — meaning regulators can get them quickly without special procedures. Years 3+ can be on slower/colder storage but still retrievable on request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can RustFS or MinIO handle FINRA-grade Object Lock?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RustFS supports S3 Object Lock in both Governance and Compliance modes as of v1.2+. MinIO also supports Object Lock. Both implement the same S3 API (&lt;code&gt;put-object-retention&lt;/code&gt;, &lt;code&gt;get-object-retention&lt;/code&gt;, &lt;code&gt;put-object-legal-hold&lt;/code&gt;). For production FINRA workloads, verify your specific version supports the full Object Lock API surface, test retention enforcement with an adversarial attempt (try deleting a locked object as root), and document the configuration for your examiner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens during a FINRA exam if my storage doesn't support Object Lock?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FINRA examiners evaluate whether your storage architecture satisfies SEC Rule 17a-4(f)'s immutability requirement. If you're using standard (non-WORM) object storage, you'll need to demonstrate an alternative compliant approach — such as a complete audit trail system that logs every modification/deletion with timestamps and identities (the "audit trail alternative" added in the October 2022 rule amendments). This is harder to defend in an exam than true WORM storage because you're proving a process works rather than demonstrating that the technology makes violations impossible.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Not legal advice. Consult qualified securities counsel. Last updated: July 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>finra</category>
      <category>compliance</category>
      <category>s3</category>
      <category>rustfs</category>
    </item>
  </channel>
</rss>
