RustFSinfrastructure

Why RustFS for Object Storage

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

v1.0·12 min read·Kenneth Pernyér
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

OptionProsCons
AWS S3The 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
MinIOS3-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
RustFSS3-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

Future Outlook

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.

Our Decision

Why we chose this

  • No garbage collectorRustFS 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 compatibilityDrop-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 storageSelf-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 runtimeConverge 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 credentialsRun RustFS in Docker. Your tests hit a real S3-compatible API, not a mock. No AWS credentials needed for local development.

×Trade-offs we accept

  • Production maturityRustFS 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 ecosystemMinIO has years of integrations, management UIs, and operator tooling. RustFS will get there, but it is not there yet.
  • S3 compatibility edge casesNo S3-compatible system is 100% identical to AWS S3. Test your specific workload. ACLs, multipart uploads, and advanced features may differ.

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.

Recommendation

Set up the ObjectStore abstraction:

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

// 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:

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:

# 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.

Examples

src/storage/mod.rsrust
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.rsrust
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.ymlyaml
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.

Related Articles

Stockholm, Sweden

Version 1.0

Kenneth Pernyér signature