← Mechanics
RustFS v1.0 12 min read

Why RustFS for Object Storage

Code to S3 semantics, not to a vendor — then own the storage layer when you are ready

rustfsobject-storages3miniorustinfrastructureself-hosted

The Problem

S3 won the API for object storage. Every cloud provider supports it. Every tool speaks it. It is the TCP/IP of blobs.

But the API is not the infrastructure. When you use AWS S3, you rent someone else's storage cluster. When you use GCS with the S3 XML interop, you rent Google's. Your data, their machines, their pricing, their rules.

For development, you need a local bucket — fast, offline, no cloud credentials. For production, you might start on AWS S3 or GCS. But later, when you want control over your data, your costs, your compliance boundaries — you want to run your own S3-compatible cluster.

MinIO was the answer for years. Written in Go, S3-compatible, battle-tested. It works. But it carries the Go runtime, the Go garbage collector, and Go's memory model. For a storage engine — the thing that holds your data — we want the same properties we want everywhere in the Converge stack: no garbage collector, no runtime overhead, memory safety at compile time.

RustFS is MinIO rewritten in Rust. Same S3 API compatibility. Same distributed architecture. But compiled to native code, no GC pauses, memory-safe by construction.

The key insight is not "use RustFS everywhere." It is:

Code to an S3-compatible object-store contract. Today that contract is backed by AWS S3 or GCS. Later it can be backed by RustFS.

RustFS becomes one deployment target among several — the one you own.

Current Options

AWS S3

The original. Managed, scalable, expensive at scale.

+ Battle-tested at planetary scale
+ Deep ecosystem integration
+ Managed — no ops burden
+ 99.999999999% durability
− Vendor lock-in on pricing and policies
− Egress costs add up fast
− Your data on someone else's machines
− No local dev story without emulation

MinIO

S3-compatible, self-hosted, written in Go.

+ True S3 API compatibility
+ Self-hosted — you own the data
+ Mature, battle-tested in production
+ Good Docker story for local dev
− Go runtime and garbage collector
− GC pauses under heavy write loads
− Memory usage higher than native alternatives
− Go ecosystem for extensions

RustFS

S3-compatible, self-hosted, written in Rust. No GC, no runtime.

+ S3 API compatible — drop-in for MinIO workloads
+ Rust: no garbage collector, no runtime overhead
+ Memory-safe at compile time
+ Same distributed architecture as MinIO, native performance
− Younger than MinIO — less production mileage
− Smaller community and ecosystem
− Fewer integrations and management tools

Why RustFS

+ No garbage collector

RustFS is compiled Rust. No GC pauses, no stop-the-world events. For a storage engine handling sustained write loads, this matters — predictable latency under pressure.

+ S3 API compatibility

Drop-in replacement for MinIO and works with any S3 client. Your existing tools, SDKs, and workflows keep working. The Rust object-store crate connects without changes.

+ Own your storage

Self-hosted means your data stays on your machines. No egress costs, no vendor pricing surprises, no compliance questions about where your data lives.

+ Same language as the runtime

Converge is Rust. RustFS is Rust. When you need to debug, extend, or optimize the storage layer, it is the same language, the same tooling, the same mental model.

+ Local dev without cloud credentials

Run RustFS in Docker. Your tests hit a real S3-compatible API, not a mock. No AWS credentials needed for local development.

Trade-offs

− Production maturity

RustFS is younger than MinIO. For mission-critical production storage today, MinIO or managed S3 is the safer bet. RustFS is the bet on where things are going.

− Smaller ecosystem

MinIO has years of integrations, management UIs, and operator tooling. RustFS will get there, but it is not there yet.

− S3 compatibility edge cases

No S3-compatible system is 100% identical to AWS S3. Test your specific workload. ACLs, multipart uploads, and advanced features may differ.

Our Motivation

We evaluated RustFS because we were already using MinIO for local development and wanted the same "close to the machine" properties we have everywhere else in the Converge stack.

The architecture separates concerns cleanly:

  • SurrealDB: metadata, object references, policies, tenant ownership
  • LanceDB: vectors, indexes, retrieval artifacts
  • Object store: large files, documents, exports, training corpora, blobs
  • App code: only stores object keys/URIs, never assumes vendor-specific features

The app code never talks to S3 directly. It talks to an ObjectStore trait. The trait has implementations:

  • `S3Store` — for AWS S3 or any S3-compatible endpoint
  • `GcsXmlStore` — for GCS via XML interop + HMAC keys
  • `RustFsStore` — for self-hosted RustFS clusters

All three share the same calling pattern. Swapping backends is a config change, not a code change.

Why not just use managed S3 forever?

For many teams, managed S3 is the right answer. But Converge is an agent platform — agents generate, process, and store large volumes of data. At scale, egress costs and data sovereignty become real constraints. Owning the storage layer is not ideology; it is economics and compliance.

RustFS gives us the option. Today we use it for local dev. Tomorrow it can be production. The interface stays the same.

In Practice

src/storage/mod.rs
use anyhow::Result;
use object_store::{ObjectStore, path::Path, PutPayload};
use object_store::aws::AmazonS3Builder;
use serde::Deserialize;

#[derive(Deserialize)]
pub struct StorageConfig {
    pub endpoint: String,
    pub key_id: String,
    pub secret: String,
    pub bucket: String,
}

pub fn connect(config: &StorageConfig) -> Result<Box<dyn ObjectStore>> {
    let store = AmazonS3Builder::new()
        .with_endpoint(&config.endpoint)
        .with_access_key_id(&config.key_id)
        .with_secret_access_key(&config.secret)
        .with_bucket_name(&config.bucket)
        .with_allow_http(config.endpoint.starts_with("http://"))
        .build()?;
    Ok(Box::new(store))
}

pub async fn put_blob(
    store: &dyn ObjectStore,
    key: &str,
    data: Vec<u8>,
) -> Result<()> {
    let path = Path::from(key);
    store.put(&path, PutPayload::from(data)).await?;
    Ok(())
}

pub async fn get_blob(
    store: &dyn ObjectStore,
    key: &str,
) -> Result<Vec<u8>> {
    let path = Path::from(key);
    let result = store.get(&path).await?;
    Ok(result.bytes().await?.to_vec())
}

The ObjectStore abstraction from the apache/arrow-rs object_store crate. Same code works against AWS S3, GCS XML API, MinIO, or RustFS — only the endpoint and credentials change.

src/storage/agent_artifacts.rs
use super::{StorageConfig, connect, put_blob, get_blob};
use anyhow::Result;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
pub struct ArtifactRef {
    pub agent_id: String,
    pub key: String,
    pub content_type: String,
    pub size_bytes: u64,
}

/// Store agent output as a blob, return a reference for SurrealDB
pub async fn store_artifact(
    config: &StorageConfig,
    agent_id: &str,
    name: &str,
    data: Vec<u8>,
    content_type: &str,
) -> Result<ArtifactRef> {
    let store = connect(config)?;
    let key = format!("agents/{agent_id}/artifacts/{name}");
    let size = data.len() as u64;
    put_blob(store.as_ref(), &key, data).await?;

    Ok(ArtifactRef {
        agent_id: agent_id.to_string(),
        key,
        content_type: content_type.to_string(),
        size_bytes: size,
    })
}

Agent artifacts stored as blobs, referenced by key in SurrealDB. The storage layer is invisible — agents produce artifacts, the ObjectStore puts them somewhere, SurrealDB tracks the reference. Where "somewhere" is depends on config, not code.

docker-compose.dev.yml
services:
  rustfs:
    image: rustfs/rustfs
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      RUSTFS_ROOT_USER: minioadmin
      RUSTFS_ROOT_PASSWORD: minioadmin
    volumes:
      - rustfs-data:/data
    command: server /data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 10s
      timeout: 5s
      retries: 3

volumes:
  rustfs-data:

Local development setup. RustFS runs on port 9000 (S3 API) and 9001 (web console). Same ports and credentials as MinIO — existing scripts and tools work without changes.

Looking Forward

The storage layer follows the same pattern as the rest of the stack.

On the backend: Python gave way to Rust. No GC, no interpreter, compiler does the work. On the frontend: React gave way to Svelte. No virtual DOM, no runtime framework, compiler does the work. On storage: MinIO (Go, GC) gives way to RustFS (Rust, no GC). Same pattern.

The trend is clear: every layer moves toward compiled, native, zero-overhead. The AI writes the code, the compiler checks it, the machine runs it directly.

S3 as the universal contract

S3 is not going away. It is the POSIX of object storage — not perfect, but universal. Every serious storage system speaks it. The smart move is not to pick one backend and hope. It is to:

1. Define an internal ObjectStore abstraction 2. Make the default implementation speak S3 3. Configure endpoint + credentials per environment 4. Swap backends without changing application code

The practical trajectory:

  • Local dev: RustFS on Docker for fast, offline S3-compatible testing
  • Early production: AWS S3 or GCS (XML API + HMAC keys)
  • Later: self-hosted RustFS cluster behind the same object-store interface

You are never locked in because you never coded to vendor-specific features. You coded to S3 semantics.

One caveat: even "S3-compatible" providers are not perfectly identical. GCS interoperability is good enough for most tools, but expect edge-case differences around ACLs, multipart behavior, and advanced features. Google positions it as interoperability, not full identity with Amazon S3. Test your specific workload.

For Converge, RustFS fits naturally: Rust all the way down, from the agent runtime to the storage engine. When we need blob storage for documents, exports, training corpora, or agent artifacts, the same language, the same safety guarantees, the same performance profile.

Recommendation

Set up the ObjectStore abstraction:

A clean interface for your Rust code — implementations for S3, GCS, and RustFS all share the same contract.

```rust // In your crate use object_store::{ObjectStore, path::Path}; use object_store::aws::AmazonS3Builder;

// Configure per environment fn create_store(config: &Config) -> Box<dyn ObjectStore> { AmazonS3Builder::new() .with_endpoint(&config.endpoint) // AWS, GCS XML, or RustFS .with_access_key_id(&config.key_id) .with_secret_access_key(&config.secret) .with_bucket_name(&config.bucket) .build() .expect("valid object store config") .into() } ```

Run RustFS locally with Docker:

```bash docker run -d \ --name rustfs \ -p 9000:9000 \ -p 9001:9001 \ -e RUSTFS_ROOT_USER=minioadmin \ -e RUSTFS_ROOT_PASSWORD=minioadmin \ -v rustfs-data:/data \ rustfs/rustfs server /data --console-address ":9001" ```

Environment configs:

```toml

dev.toml — local RustFS

[object_store] endpoint = "http://localhost:9000" key_id = "minioadmin" secret = "minioadmin" bucket = "converge-dev"

prod.toml — AWS S3

[object_store] endpoint = "https://s3.eu-north-1.amazonaws.com" key_id = "${AWS_ACCESS_KEY_ID}" secret = "${AWS_SECRET_ACCESS_KEY}" bucket = "converge-prod"

prod-self-hosted.toml — your own RustFS cluster

[object_store] endpoint = "https://storage.internal.converge.zone" key_id = "${RUSTFS_KEY_ID}" secret = "${RUSTFS_SECRET}" bucket = "converge-prod" ```

Key insight: the application code never changes. Only the config changes. S3 semantics are the contract. RustFS, AWS, or GCS are deployment targets.