TempsTemps
  • Docs
  • Blog
  • Pricing
  • Enterprise
  • Security
  • Contact
Star—
TempsTemps

Open-source deployment platform with built-in error tracking, analytics, and monitoring. Runs on any VPS. No surprise bills, no data leaving your infrastructure.

  • Product
  • Features
  • Documentation
  • Changelog
  • Enterprise
  • Contact
  • Resources
  • Getting Started
  • Upgrade
  • GitHub
  • Reddit
  • Tools
  • VPS Security Scanner
  • PaaS Tax Calculator
  • Compare
  • vs Vercel
  • vs Netlify
  • vs Coolify
  • All Platforms
  • Deploy
  • Next.js
  • Node.js
  • Django
  • Laravel
  • Go
  • Rust
  • All Frameworks →
  • Legal & Compliance
  • Security & Trust
  • Data Ownership & Privacy
  • GDPR Compliance

© 2026 Temps. All rights reserved.

GitHubDocs
t
Temps

AWS S3 Charges for Egress, Requests, AND Storage — Self-Host With MinIO for $0

AWS S3 Charges for Egress, Requests, AND Storage — Self-Host With MinIO for $0

March 12, 2026 (4mo ago)

Temps Team

Written by Temps Team

Last updated March 12, 2026 (4mo ago)

Free guide

Self-Hosting Starter Kit

Everything you need to go from zero to a production-ready self-hosted server in an afternoon. No Kubernetes required.

  • VPS provider comparison (Hetzner, DO, Vultr — real 2026 prices)
  • Security hardening checklist (SSH, firewall, fail2ban)
  • Docker production setup with automated backups
  • SSL, DNS, and monitoring in 15 minutes

No spam. Unsubscribe anytime. Privacy policy

#blob-storage#s3#minio#object-storage#self-hosted#s3 compatible blob storage without aws
Back to all posts

You can add S3-compatible blob storage to any app without AWS. Use MinIO on a VPS, Cloudflare R2, Backblaze B2, or a platform like Temps that bundles S3-compatible object storage as a built-in feature alongside deployment, analytics, and error tracking — all in one self-hosted binary.

This guide covers the options, the tradeoffs, and the code to connect any of them with the standard @aws-sdk/client-s3 library. The S3 API is an open protocol, not an AWS exclusive. Your upload code works identically against any compliant backend.

TL;DR: The S3 API is a protocol, not an AWS product. MinIO, Cloudflare R2, Backblaze B2, and Temps all speak it. Presigned URLs let browsers upload directly without routing bytes through your server. Self-hosted storage behind a CDN eliminates bandwidth bills entirely.


Can I Use S3-Compatible Blob Storage Without AWS?

Yes — completely. The S3 API (a REST interface using AWS Signature Version 4 authentication) has become the universal standard for object storage. According to Gartner's 2024 Cloud Infrastructure report, over 90% of object storage solutions now advertise S3 API compatibility. That means the same @aws-sdk/client-s3 code that talks to AWS also talks to MinIO, Cloudflare R2, Backblaze B2, RustFS, and Temps — with only the endpoint URL changing.

The practical consequence: build against the S3 API once and you're never locked to a provider. Changing backends is one environment variable.


Comparison: Temps vs MinIO vs Cloudflare R2

FeatureTempsMinIOCloudflare R2
S3-compatible APIYes (RustFS-backed)Yes (native)Yes
Self-hostableYes — single Rust binaryYes — single binaryNo — managed only
Pricing model~$6/mo Cloud (Hetzner cost + 30%), free self-hostedFree OSS, commercial for enterpriseSee R2 pricing page
Object taggingVia S3 API (RustFS)YesNo — 501 NotImplemented
CDN integrationBring your own (Cloudflare free tier works)Bring your ownBuilt into Cloudflare network
Managed service optionYes — Temps CloudMinIO Cloud (see pricing page)Yes (only option)
Access controlPer-project isolationIAM-style policiesS3-compatible ACLs
LicenseApache 2.0GNU AGPL 3.0 (OSS), commercialProprietary SaaS
Includes deployment, analytics, error trackingYes — one binaryNoNo

Important on R2 object tagging: Cloudflare R2 returns 501 NotImplemented on both the x-amz-tagging upload header and PutObjectTagging. If your workflow depends on S3 object tags, R2 is not a compatible replacement. MinIO supports tagging natively. Temps uses RustFS as its S3 backend, which implements S3 object tagging at the protocol level.


What Is the S3 API?

The S3 API is a REST interface for storing and retrieving binary objects in named buckets. Authentication uses AWS Signature Version 4 (SigV4) — a cryptographic signature derived from your access key, the current timestamp, the region, and the request body. Any service that implements SigV4 correctly accepts the same SDK calls as AWS S3.

Core Operations

OperationHTTP MethodWhat It Does
CreateBucketPUT /[bucket]Create a storage namespace
PutObjectPUT /[bucket]/[key]Upload a file
GetObjectGET /[bucket]/[key]Download a file
DeleteObjectDELETE /[bucket]/[key]Remove a file
ListObjectsGET /[bucket]?list-type=2List files in a bucket
HeadObjectHEAD /[bucket]/[key]Get metadata without downloading

That covers 80% of what most apps need. Multipart uploads, lifecycle policies, and versioning cover the remaining edge cases.


Free guide

Self-Hosting Starter Kit

Everything you need to go from zero to a production-ready self-hosted server in an afternoon. No Kubernetes required.

  • VPS provider comparison (Hetzner, DO, Vultr — real 2026 prices)
  • Security hardening checklist (SSH, firewall, fail2ban)
  • Docker production setup with automated backups
  • SSL, DNS, and monitoring in 15 minutes

No spam. Unsubscribe anytime. Privacy policy

How Do You Set Up an S3-Compatible Storage Backend?

Option 1: MinIO (Self-Hosted)

MinIO is the most popular open-source S3-compatible object storage server, with over 50,000 GitHub stars.

docker run -d \
  --name minio \
  -p 9000:9000 \
  -p 9001:9001 \
  -v /data/minio:/data \
  -e MINIO_ROOT_USER=minioadmin \
  -e MINIO_ROOT_PASSWORD=your-secret-key-here \
  quay.io/minio/minio server /data --console-address ":9001"

Port 9000 serves the S3 API. Port 9001 gives you a web console. The /data volume persists files across restarts.

Option 2: Cloudflare R2 (Managed, Zero Egress Fees)

R2 is S3-compatible, lives inside Cloudflare's network, and charges no egress fees. It is a managed service — not self-hostable. Configure it via the Cloudflare dashboard, then connect with the S3 SDK using https://<account_id>.r2.cloudflarestorage.com as the endpoint.

Note: R2 does not support S3 object tagging (returns 501). If you need tags, choose MinIO or Temps instead.

Option 3: Temps (Bundled — No Separate Setup)

Temps includes S3-compatible blob storage as a built-in feature. No separate MinIO instance. No AWS credentials. No extra Docker container to manage. Storage is powered by RustFS, a high-performance S3-compatible storage engine written in Rust. You get a project-isolated bucket for every deployed app, with environment variables injected automatically.


Connecting with the AWS SDK

The key insight: you use the exact same @aws-sdk/client-s3. Only the endpoint changes.

import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";

// Works against MinIO, Cloudflare R2, Temps, or AWS S3
const s3 = new S3Client({
  region: "us-east-1", // Required by the SDK, often ignored by non-AWS backends
  endpoint: process.env.S3_ENDPOINT, // e.g. http://localhost:9000 for MinIO
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
  },
  forcePathStyle: true, // Required for MinIO and most S3-compatible services
});

// Upload a file
await s3.send(new PutObjectCommand({
  Bucket: "user-uploads",
  Key: `avatars/${userId}.webp`,
  Body: fileBuffer,
  ContentType: "image/webp",
}));

// Download a file
const response = await s3.send(new GetObjectCommand({
  Bucket: "user-uploads",
  Key: `avatars/${userId}.webp`,
}));

const fileBytes = await response.Body.transformToByteArray();

Notice forcePathStyle: true. AWS uses virtual-hosted-style URLs (bucket.s3.amazonaws.com/key), but most S3-compatible services use path-style URLs (endpoint/bucket/key). This one flag handles the difference.

Connecting to Temps Specifically

When you deploy on Temps, blob storage environment variables are injected automatically:

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

// Temps injects these variables automatically for deployed apps
const s3 = new S3Client({
  endpoint: process.env.TEMPS_S3_ENDPOINT,
  region: "auto",
  credentials: {
    accessKeyId: process.env.TEMPS_S3_ACCESS_KEY,
    secretAccessKey: process.env.TEMPS_S3_SECRET_KEY,
  },
  forcePathStyle: true,
});

await s3.send(new PutObjectCommand({
  Bucket: process.env.TEMPS_S3_BUCKET,
  Key: `uploads/${file.name}`,
  Body: fileBuffer,
  ContentType: file.type,
}));

This code is fully portable. Move off Temps: change the endpoint and credentials. Your upload logic, presigned URL generation, and multipart handling stay identical.


How Do Presigned URLs Enable Direct Browser Uploads?

Presigned URLs eliminate the need to proxy file uploads through your backend. Your server generates a signed permission, the browser uploads directly to storage, and your server never touches the file bytes.

The Flow

  1. Browser asks your API: "I want to upload photo.jpg"
  2. Your API generates a presigned PUT URL (valid 5–15 minutes)
  3. Browser uploads directly to storage via fetch PUT
  4. Storage validates the signature and accepts the file

Generating Presigned URLs (Backend)

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  region: "auto",
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
  },
  forcePathStyle: true,
});

export async function generateUploadUrl(fileName: string, contentType: string) {
  const key = `uploads/${crypto.randomUUID()}/${fileName}`;

  const command = new PutObjectCommand({
    Bucket: "user-uploads",
    Key: key,
    ContentType: contentType,
  });

  const url = await getSignedUrl(s3, command, {
    expiresIn: 600, // 10 minutes
  });

  return { url, key };
}

Uploading from the Browser

async function uploadFile(file: File) {
  // Step 1: Get a presigned URL from your API
  const response = await fetch("/api/upload-url", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ fileName: file.name, contentType: file.type }),
  });

  const { url, key } = await response.json();

  // Step 2: Upload directly to storage — your server never touches the bytes
  await fetch(url, {
    method: "PUT",
    headers: { "Content-Type": file.type },
    body: file,
  });

  return key;
}

Presigned URLs use SigV4, which is part of the S3 protocol specification. Any compliant service — MinIO, Temps, Backblaze B2 — generates and validates presigned URLs the same way. The @aws-sdk/s3-request-presigner package works unmodified across all of them.

Security Rules for Presigned URLs

  • Short expiration. Five to fifteen minutes is enough for an upload.
  • Validate content type server-side. Restrict the content type in the presigned URL to prevent uploading executables when you expect images.
  • Use unique keys. Never let users control the full object key. Prefix with a UUID.
  • Set size limits. S3-compatible services support content-length conditions in presigned URLs to reject oversized files before they finish uploading.

Free guide

Self-Hosting Starter Kit

Everything you need to go from zero to a production-ready self-hosted server in an afternoon. No Kubernetes required.

  • VPS provider comparison (Hetzner, DO, Vultr — real 2026 prices)
  • Security hardening checklist (SSH, firewall, fail2ban)
  • Docker production setup with automated backups
  • SSL, DNS, and monitoring in 15 minutes

No spam. Unsubscribe anytime. Privacy policy

What About Multipart Uploads for Large Files?

The S3 protocol supports multipart uploads for files larger than 5 MB. AWS recommends multipart for anything over 100 MB.

import {
  S3Client,
  CreateMultipartUploadCommand,
  UploadPartCommand,
  CompleteMultipartUploadCommand,
} from "@aws-sdk/client-s3";

async function multipartUpload(s3: S3Client, bucket: string, key: string, file: Buffer) {
  const { UploadId } = await s3.send(new CreateMultipartUploadCommand({
    Bucket: bucket,
    Key: key,
  }));

  const partSize = 10 * 1024 * 1024; // 10MB parts
  const parts = [];

  for (let i = 0; i < file.length; i += partSize) {
    const partNumber = Math.floor(i / partSize) + 1;
    const chunk = file.slice(i, i + partSize);

    const { ETag } = await s3.send(new UploadPartCommand({
      Bucket: bucket,
      Key: key,
      UploadId,
      PartNumber: partNumber,
      Body: chunk,
    }));

    parts.push({ PartNumber: partNumber, ETag });
  }

  await s3.send(new CompleteMultipartUploadCommand({
    Bucket: bucket,
    Key: key,
    UploadId,
    MultipartUpload: { Parts: parts },
  }));
}

This same code works against AWS S3, MinIO, Cloudflare R2, and Temps. Only the endpoint changes.

For browser-based multipart uploads, libraries like @uppy/aws-s3-multipart handle splitting, parallel uploading, and resuming failed parts across any S3-compatible backend.


How Does CDN Caching Work with S3-Compatible Storage?

Serving files directly from your storage backend works for low traffic. Once you're handling thousands of requests per second, a CDN adds a critical caching layer.

Browser → CDN (Cloudflare/CloudFront) → S3-Compatible Storage
   ↑                |
   └── Cache HIT ───┘  (File served from CDN edge, storage not hit)

Set Cache-Control headers when uploading:

await s3.send(new PutObjectCommand({
  Bucket: "public-assets",
  Key: "images/hero.webp",
  Body: imageBuffer,
  ContentType: "image/webp",
  CacheControl: "public, max-age=31536000, immutable",
}));

For user-generated content that changes, use content-addressed keys (include a hash in the filename so new versions get new URLs) with shorter cache times.

Cloudflare R2: Zero egress fees and built into Cloudflare's CDN network. No egress billing is a real advantage for read-heavy workloads. Just note: R2 is managed-only and lacks object tagging support.

MinIO behind Cloudflare free tier: Achieves the same egress-free result with full control over your data and no storage vendor lock-in.

If you're running a deployment platform like Temps, storage running on the same server as your app eliminates network latency between app and storage. The CDN handles global distribution while your origin serves from a fast local path.


How Much Does Self-Hosted Storage Actually Save?

AWS S3 charges $0.023/GB for storage and $0.09/GB for bandwidth. For an app storing 100 GB with 500 GB of monthly downloads, that's $2.30 for storage plus $45 for bandwidth — $47.30/month. Most of that cost is bandwidth, not storage.

ProviderStorage CostBandwidth CostMonthly Total
AWS S3$2.30$45.00$47.30
Cloudflare R2See pricing page$0.00See pricing page
Backblaze B2 + CloudflareSee pricing page$0.00See pricing page
MinIO on a VPS~$1.00*$0.00**~$6.00
Temps (built-in storage)IncludedIncluded~$6.00***

*Portion of VPS cost allocated to storage. **Behind CDN. ***Temps Cloud pricing covers compute, storage, and all built-in platform features.

The bandwidth trap is what catches developers off guard. S3's storage pricing looks reasonable until your files get popular. Self-hosted storage behind a CDN sidesteps this entirely.

When AWS S3 Still Makes Sense

  • You need 11 nines of durability (99.999999999%) and don't want to manage replication
  • You're already deep in the AWS ecosystem (IAM, Lambda, CloudFront)
  • Your team lacks ops bandwidth to maintain another service
  • You need out-of-the-box compliance certifications (SOC 2, HIPAA)

For most indie hackers, startups, and small teams, self-hosted storage is more than durable enough, and the cost difference pays for itself in month one.


How Does Temps Handle Blob Storage?

Temps includes S3-compatible blob storage as a built-in feature. No separate MinIO instance. No AWS credentials. No additional configuration needed.

Three verified capabilities from the Temps blob crate:

  1. S3-compatible API via RustFS — Temps uses RustFS (rustfs/rustfs:1.0.0-beta.6), a high-performance S3-compatible storage engine written in Rust, as the blob backend. The RustFS source notes 2.3x faster throughput than MinIO for small object payloads.

  2. Per-project isolation — each deployed project gets its own isolated storage namespace. Your files cannot bleed into another project's bucket.

  3. Full blob operation set — put, delete, head, list, download, and copy operations are all implemented, with presigned URL generation for direct browser uploads.

What Else Is Included

  • Dashboard file browser — view, upload, and manage files through the Temps UI
  • Automatic environment variables — TEMPS_S3_ENDPOINT, TEMPS_S3_ACCESS_KEY, TEMPS_S3_SECRET_KEY, TEMPS_S3_BUCKET injected into deployed apps
  • Self-host free — Apache 2.0; run it on any Linux server
  • Temps Cloud at ~$6/mo — Hetzner cost + 30% margin, no per-seat fees, no bandwidth bills; storage is included in that price

Temps replaces Vercel (deployment), PostHog/Plausible (analytics), FullStory (session replay), Sentry (error tracking), and Pingdom (uptime monitoring) — all from a single Rust binary with a git-push workflow. Blob storage is one piece of that unified platform.


Free guide

Self-Hosting Starter Kit

Everything you need to go from zero to a production-ready self-hosted server in an afternoon. No Kubernetes required.

  • VPS provider comparison (Hetzner, DO, Vultr — real 2026 prices)
  • Security hardening checklist (SSH, firewall, fail2ban)
  • Docker production setup with automated backups
  • SSL, DNS, and monitoring in 15 minutes

No spam. Unsubscribe anytime. Privacy policy

FAQ

Is S3-compatible storage as reliable as AWS S3?

AWS S3 offers 99.999999999% (eleven nines) durability through massive cross-region replication. Self-hosted solutions like MinIO achieve high durability with erasure coding across multiple drives, but they won't match AWS's scale of redundancy. For most apps, a single-server setup with regular backups is more than sufficient. The real question is whether you need eleven nines or whether five nines — trivial to achieve with daily backups — covers your use case.

Can I migrate existing files from AWS S3 to a self-hosted backend?

Yes. Tools like rclone and mc (MinIO Client) sync entire buckets between any two S3-compatible endpoints. Run rclone sync s3:my-bucket minio:my-bucket and every object copies with metadata preserved. Both tools support parallel transfers and resume-on-failure. A 100 GB bucket typically migrates in under an hour on a standard VPS connection.

Do presigned URLs work the same across all S3-compatible services?

Presigned URLs use SigV4, part of the S3 protocol specification. Any compliant service — MinIO, Temps, Backblaze B2 — generates and validates them identically. The @aws-sdk/s3-request-presigner package works without modification across all of them. The only difference is the base URL in the signed output.

Does Cloudflare R2 support object tagging?

No. Cloudflare R2 returns 501 NotImplemented on both the x-amz-tagging upload header and the PutObjectTagging API call. If your workflow depends on S3 object tags, use MinIO or Temps instead.

How much storage can a single VPS handle?

A Hetzner CX22 with 40 GB of NVMe storage costs around $4/month. Add a 1 TB storage volume for approximately $4.35/month and you have over a terabyte of S3-compatible storage for under $9/month total. MinIO on that setup can sustain 1 Gbps throughput for reads and writes — enough to serve over 10,000 image downloads per minute. Most apps won't outgrow a single VPS for years.

What S3-compatible backends work besides MinIO?

  • SeaweedFS — distributed, optimized for billions of small files, with S3 gateway
  • Ceph RADOS Gateway — enterprise-grade, complex to operate
  • GarageHQ — lightweight, designed for self-hosters
  • LakeFS — adds Git-like versioning on top of S3 storage
  • RustFS — Rust-native, used internally by Temps

For most developers, MinIO hits the sweet spot between features and operational simplicity. If you want storage bundled with deployment and observability, Temps removes the need to manage the storage layer entirely.