# Runsite — Full Content Reference

> European cloud platform for deploying web apps, PostgreSQL, Redis and object storage from a GitHub repository. All infrastructure runs in Germany.

Runsite is a deployment platform for web apps, databases, and background jobs. Connect a GitHub repository, click deploy, and your app is live — with a managed database, automatic SSL, and a public URL. No Dockerfiles, no YAML, no AWS console. All infrastructure runs in the European Union (Germany) with a signed GDPR Data Processing Agreement available on every plan. Currently in Public Beta.

This is the extended reference. The concise overview is at https://runsite.app/llms.txt.

## What is Runsite?

Runsite is a Platform-as-a-Service (PaaS) for deploying applications. It builds and deploys your app on every git push and provides managed PostgreSQL databases, Redis-compatible caching, S3-compatible object storage, transactional email, and custom domains — all from a single dashboard. It is designed for indie developers and small teams who want to focus on building products instead of managing servers.

Billing is pay-per-resource and metered hourly: you pay only for the hours each service runs. There are no per-seat fees, no bandwidth/egress surcharges, and a hard spending limit that pauses services before you exceed your monthly cap. Every new account starts with €5 of credit, valid for 30 days and requiring no credit card; static sites are free with no time limit. There is no perpetual free tier for web services, databases or caching — those start at €3/month (€5 for Redis).

## Web Services

Long-running web servers, APIs, and backends. Push your repo and Runsite builds and serves it with auto-scaling, zero-downtime rolling deploys, and free SSL. Supports Node.js, Python, Go, Ruby, Rust, Java, PHP, .NET, and any Docker container. Frameworks auto-detected include Next.js, Nuxt, Django, Flask, Rails, Express, FastAPI, and Spring Boot.

| Plan | Monthly | vCPU | RAM | Disk | Bandwidth |
|------|---------|------|-----|------|-----------|
| Nano | €3 | 0.1 | 256 MB | 1 GB | 100 GB |
| Starter | €5 | 0.5 | 512 MB | 5 GB | 1 TB |
| Standard (popular) | €12 | 1 | 1 GB | 10 GB | 2 TB |
| Pro | €25 | 1 | 2 GB | 10 GB | 5 TB |
| Pro Plus | €50 | 2 | 4 GB | 20 GB | 10 TB |
| Business | €95 | 4 | 8 GB | 20 GB | 20 TB |
| Business Plus | €180 | 8 | 16 GB | 30 GB | 30 TB |

Nano is the entry plan and is covered by the €5 starting credit. Nano services draw on a 100 GB monthly egress pool, ship a noindex header, and sleep after 14 days with no traffic. Starter and above are always-on, indexable and unmetered on egress.

URL: https://runsite.app/services/web-services

## Static Sites

Deploy Next.js, Astro, Vite, Hugo and other static frontends to a global CDN. Auto-deploy from Git, a preview deploy on every pull request, and free SSL. Free with no time limit: 100 GB bandwidth, unlimited sites, and ~20s builds. Static sites are not billed at all — no plan, no hourly charge, and they never draw on your balance or credit. Pay-as-you-go bandwidth above the included quota will be introduced after launch, with advance notice.

URL: https://runsite.app/services/static-sites

## PostgreSQL

One-click managed PostgreSQL with daily encrypted backups, connection pooling, and encryption at rest. Data never leaves the EU. Storage beyond the included quota is +€0.25/GB/month. Nano is the entry plan at €3/month; the 30-day complimentary database was withdrawn.

| Plan | Monthly | vCPU | RAM | Storage | Connections |
|------|---------|------|-----|---------|-------------|
| Nano | €3 | 0.1 | 256 MB | 1 GB | 25 |
| Starter | €5 | 0.5 | 1 GB | 5 GB | 50 |
| Standard (popular) | €14 | 1 | 2 GB | 10 GB | 100 |
| Pro | €35 | 2 | 4 GB | 20 GB | 200 |
| Pro Plus | €70 | 4 | 8 GB | 50 GB | 400 |
| Business | €130 | 8 | 16 GB | 100 GB | 500 |

URL: https://runsite.app/services/postgresql

## Redis-compatible

A drop-in Redis 7+ compatible in-memory store for caching, sessions, and queues. Includes Pub/Sub, automatic failover, TLS, and RDB + AOF persistence on every plan. The entry plan is Starter — the 50 MB complimentary instance is no longer offered.

| Plan | Monthly | Memory | Connections |
|------|---------|--------|-------------|
| Starter | €5 | 256 MB | 250 |
| Standard (popular) | €13 | 1 GB | 1,000 |
| Plus | €25 | 2 GB | 2,000 |
| Pro | €65 | 5 GB | 5,000 |
| Max | €125 | 10 GB | 10,000 |

URL: https://runsite.app/services/redis

## S3 Object Storage

S3-compatible object storage for user uploads, backups, and assets. Pay-as-you-go: €0.025 per GB per month, with 5 GB included at no charge and no credit card required. Egress is not billed at any size. Stop using it and the invoice for storage is €0.

URL: https://runsite.app/services/s3-storage

## Transactional Email

Send receipts, password resets, and alerts. Pay-as-you-go: €1 per 1,000 emails, with 3,000 emails per month included at no charge (resets at the start of each calendar month).

URL: https://runsite.app/services/email

## Cron Jobs (coming soon)

Scheduled background jobs. Not live yet — the runtime and dashboard are still in development.

URL: https://runsite.app/services/cron-jobs

## Billing and Included Features

Runsite bills per resource, metered hourly — you pay only for the hours each service runs. Every plan includes, at no extra cost: automatic SSL certificates, auto-deploy from Git, unlimited team members (no per-seat fees), custom domains, real-time logs and metrics, and rollbacks. Users set a hard spending limit; services pause when the cap is reached, so there are no surprise bills. Every new account starts with €5 of credit, valid for 30 days and requiring no credit card — enough to run a Nano web service for about 30 days, or a Nano web service plus a Nano PostgreSQL for about 25. Until you top up, you can run one service of each type on Nano or Starter. When the credit is spent, services stop and your data is kept until you add balance. Static sites and the 5 GB object-storage allowance cost nothing with no time limit and never draw on the credit; everything else is billed by the hour.

## Refunds and Balance

There is no subscription to cancel. You top up a prepaid balance and hourly charges are drawn from it; deleting a service in the dashboard stops its charges immediately.

- **14-day guarantee:** within 14 days of your first top-up, the part of that top-up still unused on your paid balance is refundable, no questions asked. It applies once per customer and only to the first top-up.
- **Not refundable:** hourly charges already deducted for time your services ran, and any other charges already incurred.
- **Starting credit:** the €5 credit is not a payment, is spent before your paid balance, and is never part of a refund.
- **EU right of withdrawal:** consumers resident in the EU or EEA may withdraw from a distance contract within 14 calendar days of a top-up under Directive 2011/83/EU, without giving a reason.
- **Merchant of Record:** top-ups are processed by our payment processor, who issues invoices and receipts.

Requests go to support@runsite.app. Full policy: https://runsite.app/refund

## Data Residency and Compliance

All compute, databases, cache, storage, and email run in the European Union (Germany). Data never leaves EU borders. GDPR compliance is built in on every plan, and a signed Data Processing Agreement (DPA) is available at no extra cost on request.

## Who Runsite Is For and Where It Is Available

Runsite is a European hosting platform, built first for developers, startups, agencies, and small teams in the European Union and the EEA, and for anyone outside Europe who deliberately wants infrastructure and data inside the EU.

It is a strong fit for EU/EEA-based companies, freelancers, and indie developers (UK and Switzerland included), for products serving European users where GDPR data residency and a signed DPA are hard requirements, for teams that want euro pricing and EU invoicing, and for applications whose traffic is predominantly European.

It is a poor fit for workloads that need low latency for users in North America, Asia-Pacific, South Asia, Africa, or Latin America, because Germany is the only region and round-trip times from outside Europe are correspondingly high. It is also a poor fit for multi-region or global edge deployments, and for customers who need billing in a currency other than the euro or data residency outside the EU.

Runsite does not provide services to countries or territories subject to comprehensive EU, UN, UK, or US sanctions — as of August 2026: Russia, Belarus, Iran, North Korea, Cuba, Myanmar, and the occupied territories of Ukraine — nor to any person or company designated on an applicable sanctions list. Accounts identified as falling into either category are suspended and payments from those jurisdictions are declined. Section 5 of the Terms of Service (https://runsite.app/terms) sets out the full sanctions and export-control terms.

## Developer Tools

### Runsite CLI

Command-line tool for deploying services, reading logs and managing environment variables from a terminal or CI. Installs on macOS, Linux and Windows; authenticates with an API key, read from RUNSITE_API_TOKEN in CI.

URL: https://runsite.app/tools/cli

### Runsite MCP server

Hosted Model Context Protocol server at https://mcp.runsite.app/mcp, giving AI coding agents 22 tools to inspect services, read logs and deployments, and deploy or restart. Authentication is a scoped API key sent as an Authorization Bearer header; the transport is Streamable HTTP, so it works with Claude Code, Cursor, VS Code and any compliant client. Destructive operations — setting a secret, rolling back, deleting a service — require the owner's approval in the dashboard, and secret values are never returned to the agent.

URL: https://runsite.app/tools/mcp

## Machine-Readable Access

- **Markdown twins.** Every page has a Markdown representation at the same path plus `.md`: https://runsite.app/pricing.md, https://runsite.app/blog/cold-starts.md, https://runsite.app/index.md for the homepage. Requesting a page with `Accept: text/markdown` returns the same Markdown.
- **Concise overview:** https://runsite.app/llms.txt, mirrored at https://runsite.app/.well-known/llms.txt
- **RFC 9264 linkset:** https://runsite.app/.well-known/api-catalog
- **Sitemap:** https://runsite.app/sitemap.xml
- **Crawler policy:** https://runsite.app/robots.txt — every retrieval and training crawler is allowed, with Content Signals declaring search=yes, ai-input=yes, ai-train=yes.

## Guides and Comparisons

Thirty-six long-form technical articles on deployment, managed databases, caching, object storage, scheduling and EU data protection. Written by Teo Marquardt, the platform engineer who operates the services they describe. Each one is a standalone reference rather than a product page.

### Deployment

- [Blue-Green vs Rolling Deployments: What Zero Downtime Costs You](https://runsite.app/blog/blue-green-vs-rolling-deployments): Both strategies promise your users never see the release. The difference that decides between them is how many versions of your code are talking to one database at the same time.
- [What Are Cold Starts — and Why Your Free-Tier App Keeps Falling Asleep](https://runsite.app/blog/cold-starts): Cold starts make the first request to a sleeping app painfully slow. Here's why free hosting tiers spin down, what it costs you, and how to keep an app warm without paying for idle capacity.
- [Coolify and Self-Hosted PaaS vs Managed: What You're Taking On](https://runsite.app/blog/coolify-vs-managed-paas): What you actually take on when the PaaS layer runs on your own server: the maintenance ledger, the one box that holds everything, and when it stops paying off.
- [8 European Heroku Alternatives in 2026 — and How to Check One Really Is European](https://runsite.app/blog/european-heroku-alternative): Eight Heroku alternatives operated by European companies, checked on the column the lists leave out: which company you contract with, and where it is registered.
- [Git Push to Deploy: How Modern Web App Deployment Actually Works](https://runsite.app/blog/git-push-to-deploy): What actually happens between git push and live traffic: build artifacts, health-gated releases, why a rollback isn't a rebuild, and the four things a deploy pipeline can't do for you.
- [PaaS vs IaaS: Where the Responsibility Line Actually Falls](https://runsite.app/blog/paas-vs-iaas-responsibility): Every diagram draws one line between you and your provider. There are three, they fall in different places, and the one that matters legally is not on any of the diagrams.
- [9 Railway Alternatives in 2026 — and What You're Actually Detaching From](https://runsite.app/blog/railway-alternatives): Nine platforms that replace Railway, compared by who operates them and how they bill — plus the four things inside your project that decide how hard the move is.
- [Render vs Railway: Pricing, Free Tiers, and the Question Neither Answers](https://runsite.app/blog/render-vs-railway): Every comparison argues per-second billing against fixed instances. That axis is real, and it is not the one that decides where your data lives.
- [10 Vercel Alternatives in 2026 for Full-Stack Apps, Not Just the Frontend](https://runsite.app/blog/vercel-alternatives): Ten platforms that replace Vercel, compared on the thing that decides it: whether each one runs a long-lived process, and who operates it.
- [VPS vs PaaS Cost: What a €5 Server Actually Costs You](https://runsite.app/blog/vps-vs-paas-cost): The price gap between a raw VPS and a managed platform is smaller than the pricing pages suggest.

### Databases

- [Managed PostgreSQL for Developers: What the Host Actually Takes Off Your Plate](https://runsite.app/blog/managed-postgresql-for-developers): Installing Postgres takes a minute. Running it in production for three years is the real job.
- [Connection Pooling with PgBouncer: What Ports 5432 and 6432 Really Do](https://runsite.app/blog/pgbouncer-connection-pooling): You switched to port 6432 to stop the connection errors, and now prepared statements throw and SET doesn't stick.
- [pgvector: Storing and Searching AI Embeddings in PostgreSQL](https://runsite.app/blog/pgvector-embeddings-postgresql): Your app called an embeddings API and got back 1,536 numbers per document.
- [Point-in-Time Recovery: How WAL Archiving Saves You After a DROP TABLE](https://runsite.app/blog/postgres-point-in-time-recovery): Your nightly backup answers "do we have a copy?" Point-in-time recovery answers "can we go back to 3:06pm?" What the write-ahead log stores, how PITR replays it, and what it still cannot undo.
- [PostgreSQL "FATAL: sorry, too many clients already" — What It Means and How to Fix It](https://runsite.app/blog/postgres-too-many-clients-already): Your app is suddenly throwing 500s and the database logs are full of "too many clients already." What the error really means, how to see what is hogging your connections, and the fix that holds: pooling, not a bigger number.
- [How to Automate PostgreSQL Backups to S3 on a Schedule](https://runsite.app/blog/postgresql-backup-to-s3): A managed database backs itself up daily — but a second, portable copy in your own S3 bucket is yours to keep.
- [Self-Hosted vs Managed PostgreSQL: The Real Cost of Ownership](https://runsite.app/blog/self-hosted-vs-managed-postgresql): The €5 VPS looks cheaper than the €14 managed plan — until you price the hours. A line-by-line cost of ownership for self-hosting Postgres versus managed, the risk nobody puts on the invoice, and when self-hosting actually wins.

### Caching

- [Cache-Aside, Write-Through, Write-Behind: Choosing a Caching Pattern](https://runsite.app/blog/cache-aside-write-through-write-behind): Six names for what is really two decisions. The axes people conflate, the race that makes cache-aside delete instead of update, and why every canonical write-behind implementation is deprecated.
- [429 Too Many Requests: Rate Limiting from Both Sides](https://runsite.app/blog/rate-limiting-429-retry-after): You are usually on both ends of this at once — limited by somebody's API and limiting your own.
- [OOM command not allowed: Redis maxmemory and Eviction Policies](https://runsite.app/blog/redis-oom-command-not-allowed): Redis is refusing writes and still serving reads. The error is not a crash, it is a policy doing exactly what you configured, and two defaults nobody chose put you there.
- [Redis vs Database for Sessions: What Actually Differs](https://runsite.app/blog/redis-vs-database-for-sessions): Every result on the first page compares speed. The choice is made by four other things: who deletes expired sessions, how you log someone out everywhere, what a restart costs, and how long the copies live.
- [Redis vs Memcached: When the Simpler Cache Is Still the Right One](https://runsite.app/blog/redis-vs-memcached): The comparison everyone links to is a Stack Overflow question closed as opinion-based fourteen years ago.
- [When to Use Redis as a Cache (and When It Makes Things Worse)](https://runsite.app/blog/when-to-use-redis-cache): A cache is a second copy of your data that is allowed to be wrong. This is the decision frame: what to cache, what never to, the four failures that only appear in production, and why a cache full of sessions is a compliance object.

### Storage

- [MinIO Alternatives: What to Run Now That the Repository Is Archived](https://runsite.app/blog/minio-alternatives): The MinIO repository is archived and read-only.
- [AWS S3 Alternatives Without Egress Fees: What to Check Before You Switch](https://runsite.app/blog/s3-alternatives-no-egress-fees): Zero-egress storage moves the meter, it doesn't remove it. What providers bill instead of transfer, why the saving only appears when bytes leave from the bucket, and which workloads actually get cheaper by leaving S3.
- [S3-Compatible Object Storage: How It Works and How to Choose a Provider](https://runsite.app/blog/s3-compatible-object-storage): "S3" names three separate things: a product, a storage model, and an API.
- [S3 Egress Fees: Why Downloading Your Own Data Costs So Much](https://runsite.app/blog/s3-egress-fees): Storage is the cheap line on an object-storage bill. Egress, the charge for data leaving the bucket, is where the money actually goes.
- [Presigned URLs: Share and Upload Private S3 Files Without Handing Out Your Keys](https://runsite.app/blog/s3-presigned-urls): A presigned URL isn't a token the storage issued you. It's a signature you computed yourself, and almost everything people get wrong about expiry, revocation, and safety follows from that.
- [Uploading User Files to S3: Keys, Multipart, and What to Retry](https://runsite.app/blog/upload-files-to-s3): Uploading a file is three lines of SDK code, and they stay correct until a real user shows up with a 3 GB video on hotel wifi.

### Compliance

- [EU Hosting for Developers: Data Residency, GDPR, and DPAs in Practice](https://runsite.app/blog/eu-hosting-for-developers): A customer asks where their data lives, or a deal stalls on a DPA request, and suddenly "we host in Europe" isn't a good enough answer.
- [EU vs US Hosting: Latency, Law, and Cost](https://runsite.app/blog/eu-vs-us-hosting): Choosing between EU and US hosting isn't a contest for the "better" region. It's three separate trade-offs — latency, legal exposure, and cost — and each one points at a different thing.
- [Where to Store EU User Data: What GDPR Actually Requires](https://runsite.app/blog/gdpr-data-residency): A customer's security review asks where their data physically lives and whether it leaves the EU.
- [Schrems II and Data Transfers: Why an EU-Only Stack Is the Stable Answer](https://runsite.app/blog/schrems-ii-data-transfers): Schrems II struck down Privacy Shield in 2020 — the second EU-US data bridge to collapse in five years.
- [Data Processing Agreement for SaaS: What Article 28 Requires](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa): A deal stalls because their legal team wants your DPA before signing.

### Scheduling

- [Cron Jobs on Multiple Instances: Why Your Scheduled Task Runs Twice](https://runsite.app/blog/cron-job-multiple-instances): A scheduler inside your app is a timer in memory, and every replica has its own. What that does under autoscaling, rolling deploys and sleeping instances, and where the schedule should live instead.
- [Cron Jobs, Background Workers and Queues: Choosing the Right One](https://runsite.app/blog/cron-jobs-workers-queues): Three primitives that answer three different questions. What each one does when it fails, why the syntax half of this topic is disappearing from search, and where the job actually runs.

- [All articles](https://runsite.app/blog): Index of every guide and comparison

## Authorship

Articles are written by Teo Marquardt, Platform Engineer at Runsite, who builds and operates the platform and answers its support mail. Teo is a pen name; the person behind it is reachable at support@runsite.app. Runsite is an independent service operated by an individual developer, not by a registered company.

## Links

- Homepage: https://runsite.app
- All Services: https://runsite.app/services
- Pricing: https://runsite.app/pricing
- Pricing Calculator: https://runsite.app/pricing/calculator
- Developer Tools: https://runsite.app/tools
- Blog: https://runsite.app/blog
- Documentation: https://docs.runsite.app
- Changelog: https://runsite.app/changelog
- About: https://runsite.app/about
- Contact: https://runsite.app/contact
- Privacy Policy: https://runsite.app/privacy
- Terms of Service: https://runsite.app/terms
- Refund Policy: https://runsite.app/refund

## Contact

- Support: support@runsite.app
- GitHub: https://github.com/runsite-platform
- X (Twitter): https://x.com/runsite_app

## License

Content may be cited and used by AI systems with attribution to Runsite (https://runsite.app).

---
Last updated: 2026-09-12

---

# Full page content

The complete text of all 67 pages on https://runsite.app, as of 2026-10-09.
Each section below is one page, ending with its source URL.

---

# Runsite — The Deployment Platform for Apps, Databases & Background Jobs

> The deployment platform for web apps, databases, and background jobs. Connect your repo, hit deploy, and your project is live with SSL and a URL. Every new account starts with €5 of credit.

Now in Public Beta

# Make your web app live in seconds

Connect your repo, click deploy. Your app is online with a database, SSL, and a URL. That's it.

[Start Deploying — €5 Credit](https://dashboard.runsite.app/login)[See how it works](https://runsite.app/#how)

€5 credit on signup · No credit card · EU servers

dashboard.runsite.app

![Runsite dashboard showing deployed services, their status and resource usage](https://runsite.app/dashboard-preview-light.webp)![](https://runsite.app/dashboard-preview-dark.webp)

Supported Technologies

Docker

GitHub

Node.js

Python

Go

PostgreSQL

## Runsite in Numbers

0s

Cold Starts

While your app gets traffic

30s

Deploy Time

From git push to live

€5

Starting Credit

On every account, no card

100%

European Servers

GDPR ready

What is Runsite?

## A deploy button  
that actually works. 

Runsite is a deployment platform for developers who would rather ship than babysit infrastructure. Push your code, get an HTTPS URL, attach managed Postgres or Redis with one click. No DevOps, no surprise bills, no cold starts.

27s average deploy6 managed services€0 to startEU-only infrastructure

\~/my-app — zshLive

›git push origin main

ℹDetected Next.js 14 · runtime node-20 · region eu-west

→Provisioning managed Postgres (256 MB)

→Building image — 4 cached layers reused

→Issuing TLS certificate

✓Live in 27.4s — https://my-app.runsite.app

›

Deployedmy-app.runsite.app

Built for people who ship

### We replaced ops with a git push.

No CI pipelines to babysit. No YAML to maintain. No servers to patch. Runsite watches your branch, builds a container, issues SSL, wires up a managed database. Bring a Dockerfile if you want, otherwise we auto-detect your stack. The whole loop in about thirty seconds.

* Push any branch, get a preview URL on every PR
* Managed Postgres, Redis, S3: one dashboard, one bill
* EU-only servers, hard spending caps, humans on support

The old waytired

* Kubernetes YAML spaghetti
* CI/CD pipelines you have to babysit
* Separate billing for every service
* Cold starts that kill live demos
* Per-seat fees that punish growth

The Runsite waywired

* git push, your app is live
* Managed Postgres, Redis, S3 built-in
* One bill, one dashboard, no surprises
* €5 of credit on signup, no card, no cold starts
* Unlimited teammates at no extra cost

01 — Ship

### Push to deploy

Connect a repo, pick a branch, get an HTTPS URL. We auto-detect your framework, or use your Dockerfile as-is. No pipelines, no AWS console.

02 — Iterate

### Zero-downtime rolls

Every git push triggers a zero-downtime deploy. Broke something? Roll back to the last green build in one click.

03 — Trust

### EU-hosted, GDPR-ready

Every server sits in the EU. Every account ships with a signed DPA. Every budget has a hard cap that actually stops the meter.

Everything included

## One platform, zero headaches

Stop juggling services. Everything your app needs is already here.

### Push & Ship

Connect GitHub, push code, it's live. Auto-detect frameworks, zero-downtime deploys. No YAML, no Dockerfiles, no AWS console.

### Managed Data

PostgreSQL + Redis in one click. Daily backups, connection pooling, monitoring, all included. Grab the URL and build.

### S3 Storage

S3-compatible object storage for user uploads, backups, and assets. Use your existing SDKs and it just works.

### SSL & Domains

Free HTTPS on every app. Connect your own domain. Certificates are automatic.

### Spending Limits

Set a monthly cap. Get alerts before you hit it. Services pause at the limit, so there are no surprise bills.

### Logs & Metrics

Real-time logs, CPU and memory charts. See what your app is doing right now.

How It Works

## Three clicks to production

No Dockerfiles. No YAML. No AWS console. Just your code and a deploy button.

1

Step 1

### Connect GitHub

Link your repo. Runsite detects your framework automatically: Next.js, Django, FastAPI, Rails, Go, whatever you use.

Terminal

```
$ git push origin main
```

2

Step 2

### Hit Deploy

One click. We build it, set up SSL, give you a URL. Need a database? Toggle it on and PostgreSQL is ready in seconds.

3

Step 3

### Push to update

Every git push triggers a new deploy. Your app stays live while the new version rolls out.

Services

## Everything you need to ship

Web apps, static sites, databases, storage, and more. All managed, all connected, all in one place.

### Your app, online in seconds

Ship your SaaS, API, or side project. Any framework, any language. Push your code and Runsite handles the rest. Perfect for indie devs who want to deploy, not DevOps.

Up to 8 CPU

Up to 16 GB RAM

Up to 30 TB/mo

No cold starts on active apps

#### Included Features

* Auto-deploy from GitHub
* Preview URLs for pull requests
* Zero-downtime rolling deploys
* Health checks & auto-restart
* Custom domains + free SSL
[Learn more](https://runsite.app/services/web-services)

### Static sites, shipped in seconds

Deploy your landing page, docs, or portfolio. Works with Next.js, Astro, Vite, Hugo, and more. Instant builds, global CDN, preview deploys on every PR.

\~20s build time

Global CDN

100 GB bandwidth/mo

Instant deploys

#### Included Features

* Auto-detect popular frameworks
* Preview deploys for PRs
* Custom domains + free SSL
* Headers & redirects config
* Build caching for fast deploys
[Learn more](https://runsite.app/services/static-sites)

### Managed PostgreSQL, ready to go

Spin up a database in one click. Daily backups, connection pooling, monitoring, all included. Just grab the connection string and start building.

Up to 8 CPU

Up to 16 GB RAM

Private networking

Daily backups

#### Included Features

* Automated daily backups
* One-click restore
* Built-in connection pooling
* Monitoring dashboard
* Scale vertically in one click
[Learn more](https://runsite.app/services/postgresql)

### Fast cache, zero config

In-memory store for sessions, queues, rate limiting, and real-time features. Toggle it on, grab the URL, ship faster.

Dedicated instances

Up to 10 GB memory

Sub-ms latency

Persistent storage

#### Included Features

* Redis 7+ compatible
* Pub/Sub messaging
* Automatic failover
* TLS encryption
* RDB + AOF persistence
[Learn more](https://runsite.app/services/redis)

### S3-compatible object storage

Store user uploads, backups, and static assets. S3-compatible API: use your existing tools and SDKs. No egress fees, no per-operation costs. 5 GB free.

5 GB free · €0.025/GB

Zero egress fees

S3-compatible API

AES-256 encryption

#### Included Features

* No egress fees, no operation costs
* Public & private buckets
* Presigned URLs for sharing
* Access keys with permissions
* CDN integration
[Learn more](https://runsite.app/services/s3-storage)

Coming Soon

### Scheduled tasks, managed for you

Run background jobs on a schedule: database cleanups, email digests, data syncs. Standard cron syntax, execution history, failure alerts.

Up to 2 vCPU

512 MB RAM

Every-minute scheduling

30-day history

#### Planned Features

* Standard cron expressions
* Timezone support
* Concurrency policies
* Execution logs & history
* Failure alerts

### Transactional emails, built in

Send welcome emails, password resets, and notifications directly from your app. High deliverability, templates, webhooks. No third-party service needed.

High deliverability

REST API

Template engine

Webhook events

#### Included Features

* Transactional email sending
* HTML & text templates
* Delivery webhooks
* Analytics & tracking
* Custom sender domains
[Learn more](https://runsite.app/services/email)

[View all services](https://runsite.app/services)

Pricing

## Pay for resources. Nothing else.

Hourly billing in euro: pay only for the hours your services run. Switch between monthly and hourly to see exactly what you'll spend. No per-seat fees, no bandwidth surcharges.

| Service          | Plan          | Specs                                                           | Monthly | Hourly     |
| ---------------- | ------------- | --------------------------------------------------------------- | ------- | ---------- |
| Web Services     | Nano          | vCPU: 0.1, RAM: 256 MB, Disk: 1 GB, Bandwidth: 100 GB           | 3€/mo   | 0.0041€/hr |
| Web Services     | Starter       | vCPU: 0.5, RAM: 512 MB, Disk: 5 GB, Bandwidth: 1 TB             | 5€/mo   | 0.0068€/hr |
| Web Services     | Standard      | vCPU: 1, RAM: 1 GB, Disk: 10 GB, Bandwidth: 2 TB                | 12€/mo  | 0.0164€/hr |
| Web Services     | Pro           | vCPU: 1, RAM: 2 GB, Disk: 10 GB, Bandwidth: 5 TB                | 25€/mo  | 0.0342€/hr |
| Web Services     | Pro Plus      | vCPU: 2, RAM: 4 GB, Disk: 20 GB, Bandwidth: 10 TB               | 50€/mo  | 0.0685€/hr |
| Web Services     | Business      | vCPU: 4, RAM: 8 GB, Disk: 20 GB, Bandwidth: 20 TB               | 95€/mo  | 0.1301€/hr |
| Web Services     | Business Plus | vCPU: 8, RAM: 16 GB, Disk: 30 GB, Bandwidth: 30 TB              | 180€/mo | 0.2466€/hr |
| Static Sites     | Free          | CDN: Global, Bandwidth: 100 GB, Builds: \~20s, Sites: Unlimited | 0€/mo   | 0.0000€/hr |
| PostgreSQL       | Nano          | vCPU: 0.1, RAM: 256 MB, Storage: 1 GB, Connections: 25          | 3€/mo   | 0.0041€/hr |
| PostgreSQL       | Starter       | vCPU: 0.5, RAM: 1 GB, Storage: 5 GB, Connections: 50            | 5€/mo   | 0.0068€/hr |
| PostgreSQL       | Standard      | vCPU: 1, RAM: 2 GB, Storage: 10 GB, Connections: 100            | 14€/mo  | 0.0192€/hr |
| PostgreSQL       | Pro           | vCPU: 2, RAM: 4 GB, Storage: 20 GB, Connections: 200            | 35€/mo  | 0.0479€/hr |
| PostgreSQL       | Pro Plus      | vCPU: 4, RAM: 8 GB, Storage: 50 GB, Connections: 400            | 70€/mo  | 0.0959€/hr |
| PostgreSQL       | Business      | vCPU: 8, RAM: 16 GB, Storage: 100 GB, Connections: 500          | 130€/mo | 0.1781€/hr |
| Redis-compatible | Starter       | Memory: 256 MB, Connections: 250                                | 5€/mo   | 0.0068€/hr |
| Redis-compatible | Standard      | Memory: 1 GB, Connections: 1,000                                | 13€/mo  | 0.0178€/hr |
| Redis-compatible | Plus          | Memory: 2 GB, Connections: 2,000                                | 25€/mo  | 0.0342€/hr |
| Redis-compatible | Pro           | Memory: 5 GB, Connections: 5,000                                | 65€/mo  | 0.0890€/hr |
| Redis-compatible | Max           | Memory: 10 GB, Connections: 10,000                              | 125€/mo | 0.1712€/hr |

### €5of credit on every new account

Enough to run something real, not a sandbox. No credit card to start, and static sites are never billed at all.

[Claim €5 credit](https://dashboard.runsite.app/login)

How far €5 goes

* Nano web service30 days

0.1 vCPU · 256 MB
* Web service + Nano PostgreSQL\~25 days

0.1 vCPU · 256 MB + 1 GB

Hourly billing, so the credit is spent only while services are running.

* No credit card to start
* One service of each type on Nano or Starter until you top up
* Credit gone? Services stop, data stays. Top up to bring them back

### Long-running web servers

·Apps · APIs · Backends

Push your repo, Runsite builds and serves. Auto-scale, zero-downtime rolling deploys, free SSL.

Web Services

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Disk

1 GB

Bandwidth

100 GB

[Choose Nano](https://dashboard.runsite.app/login)

Web Services

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

512 MB

Disk

5 GB

Bandwidth

1 TB

[Choose Starter](https://dashboard.runsite.app/login)

Web Services

### Standard

Popular

€12/mo

≈ €0.0164/hour, billed hourly

vCPU

1

RAM

1 GB

Disk

10 GB

Bandwidth

2 TB

[Choose Standard](https://dashboard.runsite.app/login)

Web Services

### Pro

€25/mo

≈ €0.0342/hour, billed hourly

vCPU

1

RAM

2 GB

Disk

10 GB

Bandwidth

5 TB

[Choose Pro](https://dashboard.runsite.app/login)

Web Services

### Pro Plus

€50/mo

≈ €0.0685/hour, billed hourly

vCPU

2

RAM

4 GB

Disk

20 GB

Bandwidth

10 TB

[Choose Pro Plus](https://dashboard.runsite.app/login)

Web Services

### Business

€95/mo

≈ €0.1301/hour, billed hourly

vCPU

4

RAM

8 GB

Disk

20 GB

Bandwidth

20 TB

[Choose Business](https://dashboard.runsite.app/login)

Web Services

### Business Plus

€180/mo

≈ €0.2466/hour, billed hourly

vCPU

8

RAM

16 GB

Disk

30 GB

Bandwidth

30 TB

[Choose Business Plus](https://dashboard.runsite.app/login)

Nano is the entry plan: the same 0.1 vCPU / 256 MB as before, now €3/mo and covered by your €5 starting credit. Nano services draw on a 100 GB monthly egress pool, ship a noindex header, and sleep after 14 days with no traffic. Starter and above are always-on, indexable and unmetered on egress.

### JAMstack & static frontends

·Landing pages · Docs · Portfolios

Deploy Next.js, Astro, Vite, Hugo and friends to a global CDN. Auto-deploy from Git, preview on every PR, free SSL.

Static Sites

### Free

Popular

Free

Never billed · never draws on your balance

CDN

Global

Bandwidth

100 GB

Builds

\~20s

Sites

Unlimited

[Deploy for free](https://dashboard.runsite.app/login)

Static sites are not billed. No plan, no hourly charge, and they never draw on your balance or credit. Pay-as-you-go bandwidth above the included quota will be introduced after launch. We'll notify you before any change.

### Production-ready databases

·Managed · Backups · EU-sovereign

One-click PostgreSQL with daily backups, connection pooling, encrypted at rest, never leaves the EU.

PostgreSQL

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Storage

1 GB

Connections

25

[Choose Nano](https://dashboard.runsite.app/login)

PostgreSQL

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

1 GB

Storage

5 GB

Connections

50

[Choose Starter](https://dashboard.runsite.app/login)

PostgreSQL

### Standard

Popular

€14/mo

≈ €0.0192/hour, billed hourly

vCPU

1

RAM

2 GB

Storage

10 GB

Connections

100

[Choose Standard](https://dashboard.runsite.app/login)

PostgreSQL

### Pro

€35/mo

≈ €0.0479/hour, billed hourly

vCPU

2

RAM

4 GB

Storage

20 GB

Connections

200

[Choose Pro](https://dashboard.runsite.app/login)

PostgreSQL

### Pro Plus

€70/mo

≈ €0.0959/hour, billed hourly

vCPU

4

RAM

8 GB

Storage

50 GB

Connections

400

[Choose Pro Plus](https://dashboard.runsite.app/login)

PostgreSQL

### Business

€130/mo

≈ €0.1781/hour, billed hourly

vCPU

8

RAM

16 GB

Storage

100 GB

Connections

500

[Choose Business](https://dashboard.runsite.app/login)

Storage beyond the included quota: +€0.25/GB/mo. Daily encrypted backups included. Nano is the entry plan at €3/mo. There is no longer a 30-day free database. If your credit and balance run out the database stops with its volume intact; press Start after topping up and it comes straight back. Storage is reclaimed 7 days later and the final backup is kept for 12 months.

### Sub-ms in-memory store

·Cache · Sessions · Queues

Drop-in Redis 7+ compatible store. Pub/Sub, automatic failover, TLS, RDB + AOF persistence.

Redis-compatible

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

Memory

256 MB

Connections

250

[Choose Starter](https://dashboard.runsite.app/login)

Redis-compatible

### Standard

Popular

€13/mo

≈ €0.0178/hour, billed hourly

Memory

1 GB

Connections

1,000

[Choose Standard](https://dashboard.runsite.app/login)

Redis-compatible

### Plus

€25/mo

≈ €0.0342/hour, billed hourly

Memory

2 GB

Connections

2,000

[Choose Plus](https://dashboard.runsite.app/login)

Redis-compatible

### Pro

€65/mo

≈ €0.0890/hour, billed hourly

Memory

5 GB

Connections

5,000

[Choose Pro](https://dashboard.runsite.app/login)

Redis-compatible

### Max

€125/mo

≈ €0.1712/hour, billed hourly

Memory

10 GB

Connections

10,000

[Choose Max](https://dashboard.runsite.app/login)

Redis starts at Starter. The 50 MB free instance is no longer offered. Every plan here includes RDB + AOF persistence, TLS and automatic failover, and is billed by the hour like everything else. A Starter instance runs for roughly a month on the €5 starting credit, if it is the only thing you have running.

Runsite hosts applications, not relays. VPN endpoints, proxies, VLESS / Xray / V2Ray and Shadowsocks nodes, and traffic tunnels of any kind are prohibited on every plan, paid ones included, and are blocked without notice or refund. See the Acceptable Use Policy in Section 4 of the Terms.

Pay only for what you use

## No subscription. No minimum.

Storage and email are billed by actual consumption. Stop using them and the invoice for that service is exactly **€0**, even if everything else stays running.

### S3 Object Storage

User uploads, backups, assets

Pay-as-you-go

€0.025

per GB · per month

5 GB included

Included on every account, no card required

Usage examples/ month

* 5 GBIncluded
* 100 GB€2.50
* 500 GB€12.50
* 1 TB€25
* 5 TB€125

Zero egress fees

S3-compatible API

Public + private buckets

AES-256 encryption

Billed per GB-hour. Stop using it, stop paying. No minimums, no commitments.

### Transactional Email

Receipts, password resets, alerts

Pay-as-you-go

€1

per 1,000 emails

3,000 emails / month included

Production sending unlocks after a €5 top-up

Usage examples/ month

* 3,000Included
* 5,000€2
* 10,000€7
* 25,000€22
* 50,000€47

High-deliverability EU sending

HTML + text templates

Webhooks for opens & clicks

Custom sender domains

Charged only for the emails above your included quota. Sending to external recipients requires a minimum €5 top-up; before that, mail stays inside the sandbox. No subscription, no monthly minimum.

### Set a limit. Never overspend.

Tell us your max budget. When you hit the limit, services pause, not your wallet. No surprise bills.

### Unlimited team

Invite your whole team, 2 people or 200\. No per-seat fees, ever.

SSL certificates

Auto-deploy from Git

Unlimited team members

Custom domains

Real-time logs

Rollbacks

Typical side project: 1 web service + 1 database

€6/month

Nano web (€3) + Nano Postgres (€3). Your €5 starting credit covers most of the first month. Whole team included, no per-seat fees.

FAQ · 10

## Common questions Clear answers.

Quick answers. If something's missing, just ask us.

viewing01 / 10

### Still curious?

Real humans, real answers, usually within a few hours.

[support@runsite.app](mailto:support@runsite.app)

### What languages and frameworks work?

Anything that runs in a container. Node, Python, Go, Ruby, Rust, PHP, Java, .NET. We auto-detect Next.js, Django, FastAPI, Rails, Laravel, and Spring Boot, and most projects deploy with zero config.

Have a Dockerfile? We use it as-is. Non-standard setup? Set your own build commands and env vars.

No lock-in: your code runs in standard Docker containers, so you can move to or from Runsite without rewriting anything. Full list in the documentation.

### How does the €5 starting credit work?

Every new account gets €5 of credit, valid for 30 days. No credit card, no card check, nothing to claim: it is on the balance the moment you sign up.

Charges are drawn from the credit first and from your real balance only after it is gone, and the two are always shown as separate numbers on the billing page. The credit is not a top-up: it never counts toward your lifetime spend, so it does not unlock the tiers or the production email sending that a real payment does.

What it buys: the entry Nano plans are €3/mo each. One Nano web service on its own runs the full 30 days and leaves credit unspent. A web service plus a Nano PostgreSQL costs €6 a month, so the pair runs about 25 days before the credit is gone. Top up any amount from €5 to carry it past that. Static sites are not billed at all and never touch the credit.

Two limits apply while you are running on credit alone. You get one service of each type (one web service, one PostgreSQL, one Redis, one cron job), and static sites are not counted against that. And you can only pick the entry plans: Nano, or Starter for the types that have no Nano. Standard and above need a real payment first, because €5 should not buy 8 vCPU for an afternoon. A single top-up of €5 lifts both.

When the credit runs out, or when the 30 days are up (whichever comes first), running services stop and the remainder is written off. Your real balance is never touched. Full breakdown on the pricing page.

### Do apps sleep or cold-start?

No cold starts while you're using the app. Runsite keeps containers warm as long as they receive traffic.

On Nano, the entry plan, a web service that gets zero requests for 14 consecutive days is suspended to free up shared capacity. The next incoming request wakes it back up (usually within a few seconds). Your code, environment variables, and storage are preserved.

In practice this only affects truly abandoned projects. Any live demo, bot, or side project with even occasional visitors will never sleep. Starter and above (from €5/mo) skip the inactivity sleep entirely and stay always-on with dedicated CPU.

### What happens when my credit runs out?

Your services stop. Nothing is deleted on the spot, and nothing is charged to a card you never gave us.

We email you on day 23 and day 29 of the credit, and again when services actually stop, so the wall is never a surprise. Top up any amount from €5 and press Start: a web service redeploys, a PostgreSQL database comes back with its volume exactly as it was.

Storage is held for 7 days after the stop. After that the PostgreSQL volume is reclaimed and the final backup is kept for 12 months, so a project you come back to months later can still be restored by topping up. Redis is in-memory and is not backed up, so export anything you need before the deadline.

Static sites are unaffected. They are never billed and never stop.

### How do spending limits work?

You set a monthly budget in the dashboard, say €20\. Services consume resources, we track usage against that cap in real time.

Hit 80% and you get an email. Hit 100% and services pause automatically. They stop running but aren't deleted, and nothing more gets charged. Raise the cap or wait for the next cycle to resume.

The cap is a hard stop, not a friendly email after the damage is done.

### Can I use my own domain?

Yes. Add your domain in the dashboard, point your DNS at Runsite (A record for apex, CNAME for subdomains), and SSL is issued via Let's Encrypt in minutes. Renewals are automatic.

Attach multiple custom domains to a single service. Wildcard domains work on paid plans.

Every service also gets a free \*.runsite.app subdomain, handy for staging or a quick demo. Here's how to set up a custom domain step by step.

### Are there per-seat or per-user fees?

No. No per-seat, per-user, or per-team charges. Your whole team, 2 people or 200, can deploy, manage services, and read logs at no extra cost.

You pay for the infrastructure your services actually use: CPU, memory, storage. That's a different model from Heroku or Railway, where per-seat fees snowball as teams grow.

Shared dashboards, deploy permissions, and audit logs come with every plan.

### Where are the servers?

EU only. All of them. Your app data, database backups, and object storage never leave EU borders.

Every account is GDPR-compliant by default and ships with a signed Data Processing Agreement. No negotiation, no upcharge, no sales call.

That matters if you handle EU customer data or fall under the Digital Services Act. Unlike US-based platforms that bolt on EU regions as an extra, Runsite is EU by design.

### Can I run a VPN, proxy, or tunnel on Runsite?

No. Proxies, VPN endpoints, VLESS/Xray/V2Ray and Shadowsocks nodes, and traffic tunnels of any kind are prohibited on every plan, paid ones included. Services identified as running them are blocked without notice and without a refund.

This is not a bandwidth policy we might relax later. Relay traffic is what a shared platform cannot absorb: it saturates egress that other customers paid for, and it puts our IP ranges on blocklists that take months to leave.

Runsite is built for applications: web services, APIs, background jobs, and the databases behind them. If your workload is a legitimate app that happens to open outbound connections (a scraper, a bot, a webhook relay), that is fine. Anything whose purpose is to forward third-party traffic is not. Section 4 of the Terms of Service sets this out in full.

### Which countries does Runsite serve?

Runsite is a European platform, built for European teams. The infrastructure runs entirely inside the EU, we invoice in euro, and a GDPR-compliant DPA comes with every account, so if you are looking for European hosting for a European product, this is what it was built for.

You don't have to be an EU company to sign up. Customers in the UK, Switzerland, and other countries are welcome, and their data stays in the EU either way.

We do not serve countries and territories under comprehensive EU, UN, UK, or US sanctions (as of August 2026 that means Russia, Belarus, Iran, North Korea, Cuba, Myanmar, and the occupied territories of Ukraine), or any person or company on an applicable sanctions list. Accounts we identify as falling into that category are suspended, and payments from those jurisdictions are declined. Section 5 of the Terms of Service sets this out in full.

Blog

## From the Runsite Blog

Practical guides on deploying, running databases, and keeping data in the EU.

[Deployment9 min read502 Bad Gateway in Nginx After Deploy: Read the Error LogA 502 Bad Gateway page from nginx says nothing about the cause. The matching line in error.log does: connection refused, a worker that died mid-response, a keep-alive reset or an oversized header. What each one means and how to fix it after a deploy.Oct 9, 2026Read](https://runsite.app/blog/502-bad-gateway-nginx)[Deployment9 min readECONNREFUSED 127.0.0.1: Your App Is Calling ItselfError: connect ECONNREFUSED 127.0.0.1:6379 after a deploy means your app is calling its own machine. Why the address says localhost, the ::1 variant, and how to fix each cause for Redis and Postgres.Oct 7, 2026Read](https://runsite.app/blog/econnrefused-127-0-0-1)[Deployment10 min readExit Code 137: SIGKILL, Not Always Out of MemoryExit code 137 says a process was killed with SIGKILL. It doesn't say who sent it. How to tell a memory kill from a missed SIGTERM on deploy or a failing probe, and what fixes each one.Oct 5, 2026Read](https://runsite.app/blog/exit-code-137)

### Most-read guides

* [S3 presigned URLs](https://runsite.app/blog/s3-presigned-urls)
* [Postgres: too many clients already](https://runsite.app/blog/postgres-too-many-clients-already)
* [Postgres connection pooling](https://runsite.app/blog/pgbouncer-connection-pooling)
* [cold starts on free tiers](https://runsite.app/blog/cold-starts)
* [GDPR data residency](https://runsite.app/blog/gdpr-data-residency)
* [what a DPA is](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa)

[Read all articles](https://runsite.app/blog)

---

# Services — Compute, Databases & Storage

> Everything the Runsite deployment platform runs for you: web apps, static sites, cron jobs, managed PostgreSQL, Redis, and S3-compatible object storage. One git push to deploy.

7 managed services · one platform

# All the Cloud Services You Need to Ship

Compute, databases, and storage — all managed, all connected, all in one platform. From prototype to production in minutes, hosted entirely in the EU.

[Claim €5 credit](https://dashboard.runsite.app/login)[Browse services](#compute)

### One git push

Connect a repo, push to main, watch it go live in \~30 seconds.

### EU data residency

Everything runs in Germany. A signed GDPR DPA on every plan.

### €5 starting credit

Every new account gets €5, valid 30 days. No credit card, no cold starts on active apps.

### Wired together

Compute, databases, cache, storage and mail — connected over private networking.

Runsite is a managed cloud platform that runs your whole stack from a single git push. Instead of stitching together separate providers for hosting, databases, caching, storage, and email, you get every service on one platform — wired together over private networking and billed under one roof. Every new account starts with €5 of credit and no credit card, scales as your app grows, and runs entirely in the EU (Germany) with a signed GDPR DPA on every plan. Browse the services below or compare them side by side to find the right building blocks for your project.

At a glance

## Compare all services

| Service                                   | Category  | What it's for                                                                                                                         | Pricing                        |
| ----------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| [Web Services](https://runsite.app/services/web-services)    | Compute   | Node.js, Python, Go, Ruby, Rust, or your own Dockerfile — Runsite builds it, runs it, and keeps it online on European infrastructure. | from €3/mo                     |
| [Static Sites](https://runsite.app/services/static-sites)    | Compute   | Next.js, Astro, Hugo, Vite — any generator that writes static files. Automatic builds, a global CDN, free SSL, and your own domain.   | Not billed                     |
| [PostgreSQL](https://runsite.app/services/postgresql)        | Data      | Managed PostgreSQL. Daily backups, one-click restore. Just connect and start querying.                                                | from €3/mo                     |
| [Redis](https://runsite.app/services/redis)                  | Data      | In-memory cache for sessions, queues, and real-time features. Toggle it on and go.                                                    | from €5/mo                     |
| [S3 Object Storage](https://runsite.app/services/s3-storage) | Storage   | Store files, images, and backups. S3-compatible — use the tools you already know.                                                     | Not billed · from €0.025/GB/mo |
| [Email Service](https://runsite.app/services/email)          | Messaging | Send receipts, password resets, and notifications. 3,000 emails included every month.                                                 | Not billed · from €1 / 1,000   |
| Cron Jobs                                 | Compute   | Run scheduled tasks automatically. Set a schedule, we run your code — with logs and retries built in.                                 | Coming soon                    |

Compute

## Deploy and run your applications

3 services

[ from €3/moWeb ServicesNode.js, Python, Go, Ruby, Rust, or your own Dockerfile — Runsite builds it, runs it, and keeps it online on European infrastructure.Explore service](https://runsite.app/services/web-services)[ Not billedStatic SitesNext.js, Astro, Hugo, Vite — any generator that writes static files. Automatic builds, a global CDN, free SSL, and your own domain.Explore service](https://runsite.app/services/static-sites)

Coming Soon

### Cron Jobs

Run scheduled tasks automatically. Set a schedule, we run your code — with logs and retries built in.

Data

## Managed databases and caching

2 services

[ from €3/moPostgreSQLManaged PostgreSQL. Daily backups, one-click restore. Just connect and start querying.Explore service](https://runsite.app/services/postgresql)[ from €5/moRedisIn-memory cache for sessions, queues, and real-time features. Toggle it on and go.Explore service](https://runsite.app/services/redis)

Storage

## Store and serve files at scale

1 service

[ Not billedS3 Object StorageStore files, images, and backups. S3-compatible — use the tools you already know.Explore service](https://runsite.app/services/s3-storage)

Messaging

## Send transactional emails to your users

1 service

[ Not billedEmail ServiceSend receipts, password resets, and notifications. 3,000 emails included every month.Explore service](https://runsite.app/services/email)

FAQ

## Frequently Asked Questions

Common questions about this service.

### How are Runsite services connected?

Every service runs on a single platform and is wired together over private networking — your compute, databases, cache, storage, and mail talk to each other internally without exposing traffic to the public internet. You manage them from one dashboard and pay under one bill, instead of stitching together separate providers.

### What does it cost to start?

Every new account gets €5 of credit, valid for 30 days, with no credit card required. The entry Nano plans for web services and PostgreSQL are €3/mo each. One Nano web service on its own runs the full 30 days on the credit; a web service plus a database costs €6 a month, so that pair runs about 25 days before you need to top up. Redis starts at €5/mo. Some things are never billed and never touch the credit: static sites, the first 5 GB of object storage, and 3,000 transactional emails a month. Sending mail to external recipients does require a one-time top-up of at least €5\. While you are on credit alone you run one service of each type (one web service, one PostgreSQL, one Redis, one cron job) on the entry plans, Nano or Starter. Static sites are not counted. A €5 top-up lifts both limits. When the credit is spent or expires, services stop with their data intact. Top up any amount from €5 and press Start to bring them back.

### Where is my data hosted?

Everything runs in the EU — specifically Germany. Every plan includes a signed GDPR Data Processing Agreement (DPA), so your data stays within European jurisdiction by default.

### How do I deploy to Runsite?

Connect a git repository and push to your main branch. Runsite builds your code and takes it live in about 30 seconds — no manual server setup, no separate CI pipeline to configure.

### Can I use just one service, or do I need all of them?

You can use any combination of services. Start with a single web app or database and add caching, storage, cron jobs, or email whenever you need them. Each service is billed independently, so you only pay for what you run.

---

# Blog — Guides on Deploying & Running Apps

> Practical, hand-crafted guides on deploying web apps, running managed databases, caching, scheduled jobs, object storage, and hosting in the EU — from the team building the Runsite platform.

The Runsite Blog

# Guides for Developers Who'd Rather Ship

Practical, hand-crafted writing on deploying apps, running managed databases, caching, scheduled jobs, and hosting in the EU — drawn from the experience of building and operating the Runsite platform.

[Read the latest](#latest)[Explore services](https://runsite.app/services)

Latest

[Deployment9 min read502 Bad Gateway in Nginx After Deploy: Read the Error LogA 502 Bad Gateway page from nginx says nothing about the cause. The matching line in error.log does: connection refused, a worker that died mid-response, a keep-alive reset or an oversized header. What each one means and how to fix it after a deploy.Oct 9, 2026Read](https://runsite.app/blog/502-bad-gateway-nginx)

[Deployment9 min readECONNREFUSED 127.0.0.1: Your App Is Calling ItselfError: connect ECONNREFUSED 127.0.0.1:6379 after a deploy means your app is calling its own machine. Why the address says localhost, the ::1 variant, and how to fix each cause for Redis and Postgres.Oct 7, 2026Read](https://runsite.app/blog/econnrefused-127-0-0-1)[Deployment10 min readExit Code 137: SIGKILL, Not Always Out of MemoryExit code 137 says a process was killed with SIGKILL. It doesn't say who sent it. How to tell a memory kill from a missed SIGTERM on deploy or a failing probe, and what fixes each one.Oct 5, 2026Read](https://runsite.app/blog/exit-code-137)[Deployment9 min read413 Request Entity Too Large: Which Layer Said No, and When to Stop Raising the LimitA 413 comes from whichever size limit in the request path is smallest: the CDN, the proxy, the ingress or the framework. How to tell which one answered, how to raise it, and when a large upload shouldn't go through your app at all.Oct 2, 2026Read](https://runsite.app/blog/413-request-entity-too-large)[Deployment9 min readGitHub Webhook "Timed out": Why Your Host Drops DeliveriesGitHub gives your endpoint 10 seconds and never retries on its own. Where those seconds go on a sleeping free tier, a serverless function or a deploy, and how to build a receiver that doesn't lose events.Sep 29, 2026Read](https://runsite.app/blog/github-webhook-timed-out)[Deployment12 min readMigrating Off Heroku Postgres: The Dump, the Cutover and the RollbackThree ways to get a database out of Heroku Postgres, how to work out the downtime before you commit to it, a cutover order that doesn't lose writes, and the point after which rolling back stops being free.Sep 26, 2026Read](https://runsite.app/blog/migrate-from-heroku)[Deployment12 min readPaaS Pricing Comparison 2026: One App, Priced on Seven PlatformsThe same small production app (a web service, Postgres, Redis and 100 GB of traffic) priced on Render, Heroku, Railway, Fly.io, DigitalOcean, Vercel and Runsite, for one developer and for a team of three.Sep 26, 2026Read](https://runsite.app/blog/paas-pricing-compared)[Deployment12 min read10 European Netlify Alternatives for Static Sites (2026)Ten places to host a static site in Europe instead of Netlify, compared by who operates each one, what the free tier covers, and what happens on the day it runs out.Sep 26, 2026Read](https://runsite.app/blog/netlify-alternatives-europe)[Deployment13 min read9 Render Alternatives in 2026, and What Your render.yaml Doesn't CarryNine platforms that replace Render.com, sorted by who operates them and how the bill differs from Render's, plus a line-by-line read of what a render.yaml moves and what it quietly leaves behind.Sep 26, 2026Read](https://runsite.app/blog/render-alternatives)[Scheduling14 min readCron Jobs on Multiple Instances: Why Your Scheduled Task Runs TwiceA scheduler inside your app is a timer in memory, and every replica has its own. What that does under autoscaling, rolling deploys and sleeping instances, and where the schedule should live instead.Sep 11, 2026Read](https://runsite.app/blog/cron-job-multiple-instances)[Caching16 min readRedis vs Database for Sessions: What Actually DiffersEvery result on the first page compares speed. The choice is made by four other things: who deletes expired sessions, how you log someone out everywhere, what a restart costs, and how long the copies live.Sep 9, 2026Read](https://runsite.app/blog/redis-vs-database-for-sessions)[Deployment13 min readPaaS vs IaaS: Where the Responsibility Line Actually FallsEvery diagram draws one line between you and your provider. There are three, they fall in different places, and the one that matters legally is not on any of the diagrams.Sep 7, 2026Read](https://runsite.app/blog/paas-vs-iaas-responsibility)[Scheduling14 min readCron Jobs, Background Workers and Queues: Choosing the Right OneThree primitives that answer three different questions. What each one does when it fails, why the syntax half of this topic is disappearing from search, and where the job actually runs.Sep 4, 2026Read](https://runsite.app/blog/cron-jobs-workers-queues)[Caching12 min readOOM command not allowed: Redis maxmemory and Eviction PoliciesRedis is refusing writes and still serving reads. The error is not a crash, it is a policy doing exactly what you configured, and two defaults nobody chose put you there.Sep 2, 2026Read](https://runsite.app/blog/redis-oom-command-not-allowed)[Storage12 min readMinIO Alternatives: What to Run Now That the Repository Is ArchivedThe MinIO repository is archived and read-only. Garage, SeaweedFS, RustFS and Ceph compared on what each one actually costs to operate, and the question the rest of the internet skips, which is whether you should be running object storage yourself at all.Aug 31, 2026Read](https://runsite.app/blog/minio-alternatives)[Caching13 min read429 Too Many Requests: Rate Limiting from Both SidesYou are usually on both ends of this at once — limited by somebody's API and limiting your own. One header joins the two sides, and almost nobody sends or reads it properly.Aug 28, 2026Read](https://runsite.app/blog/rate-limiting-429-retry-after)[Caching12 min readRedis vs Memcached: When the Simpler Cache Is Still the Right OneThe comparison everyone links to is a Stack Overflow question closed as opinion-based fourteen years ago. What has changed since then is not the feature list. It is the licence, the fork, and a demand curve falling 46% a year.Aug 27, 2026Read](https://runsite.app/blog/redis-vs-memcached)[Caching13 min readCache-Aside, Write-Through, Write-Behind: Choosing a Caching PatternSix names for what is really two decisions. The axes people conflate, the race that makes cache-aside delete instead of update, and why every canonical write-behind implementation is deprecated.Aug 24, 2026Read](https://runsite.app/blog/cache-aside-write-through-write-behind)[Caching12 min readWhen to Use Redis as a Cache (and When It Makes Things Worse)A cache is a second copy of your data that is allowed to be wrong. This is the decision frame: what to cache, what never to, the four failures that only appear in production, and why a cache full of sessions is a compliance object.Aug 21, 2026Read](https://runsite.app/blog/when-to-use-redis-cache)[Deployment14 min readVPS vs PaaS Cost: What a €5 Server Actually Costs YouThe price gap between a raw VPS and a managed platform is smaller than the pricing pages suggest. This itemises both sides: the hidden line items, the jobs, and who the processor is.Aug 19, 2026Read](https://runsite.app/blog/vps-vs-paas-cost)[Deployment11 min readBlue-Green vs Rolling Deployments: What Zero Downtime Costs YouBoth strategies promise your users never see the release. The difference that decides between them is how many versions of your code are talking to one database at the same time.Aug 17, 2026Read](https://runsite.app/blog/blue-green-vs-rolling-deployments)[Deployment15 min read10 Vercel Alternatives in 2026 for Full-Stack Apps, Not Just the FrontendTen platforms that replace Vercel, compared on the thing that decides it: whether each one runs a long-lived process, and who operates it.Aug 14, 2026Read](https://runsite.app/blog/vercel-alternatives)[Deployment14 min read9 Railway Alternatives in 2026 — and What You're Actually Detaching FromNine platforms that replace Railway, compared by who operates them and how they bill — plus the four things inside your project that decide how hard the move is.Aug 13, 2026Read](https://runsite.app/blog/railway-alternatives)[Deployment12 min readRender vs Railway: Pricing, Free Tiers, and the Question Neither AnswersEvery comparison argues per-second billing against fixed instances. That axis is real, and it is not the one that decides where your data lives.Aug 12, 2026Read](https://runsite.app/blog/render-vs-railway)[Deployment13 min readCoolify and Self-Hosted PaaS vs Managed: What You're Taking OnWhat you actually take on when the PaaS layer runs on your own server: the maintenance ledger, the one box that holds everything, and when it stops paying off.Aug 10, 2026Read](https://runsite.app/blog/coolify-vs-managed-paas)[Deployment12 min readGit Push to Deploy: How Modern Web App Deployment Actually WorksWhat actually happens between git push and live traffic: build artifacts, health-gated releases, why a rollback isn't a rebuild, and the four things a deploy pipeline can't do for you.Aug 7, 2026Read](https://runsite.app/blog/git-push-to-deploy)[Deployment15 min read8 European Heroku Alternatives in 2026 — and How to Check One Really Is EuropeanEight Heroku alternatives operated by European companies, checked on the column the lists leave out: which company you contract with, and where it is registered.Aug 7, 2026Read](https://runsite.app/blog/european-heroku-alternative)[Storage11 min readAWS S3 Alternatives Without Egress Fees: What to Check Before You SwitchZero-egress storage moves the meter, it doesn't remove it. What providers bill instead of transfer, why the saving only appears when bytes leave from the bucket, and which workloads actually get cheaper by leaving S3.Aug 5, 2026Read](https://runsite.app/blog/s3-alternatives-no-egress-fees)[Storage11 min readUploading User Files to S3: Keys, Multipart, and What to RetryUploading a file is three lines of SDK code, and they stay correct until a real user shows up with a 3 GB video on hotel wifi. Here's what comes after the tutorial: naming objects you can't rename, splitting large files, and knowing which failures are safe to repeat.Aug 3, 2026Read](https://runsite.app/blog/upload-files-to-s3)[Storage13 min readPresigned URLs: Share and Upload Private S3 Files Without Handing Out Your KeysA presigned URL isn't a token the storage issued you. It's a signature you computed yourself, and almost everything people get wrong about expiry, revocation, and safety follows from that. Here's the mechanism, what one looks like, and how to use it for both downloads and uploads.Jul 31, 2026Read](https://runsite.app/blog/s3-presigned-urls)[Storage11 min readS3-Compatible Object Storage: How It Works and How to Choose a Provider"S3" names three separate things: a product, a storage model, and an API. This guide separates them — how buckets actually behave, what "S3-compatible" guarantees, where that compatibility runs out, and the axes worth comparing providers on.Jul 29, 2026Read](https://runsite.app/blog/s3-compatible-object-storage)[Databases11 min readSelf-Hosted vs Managed PostgreSQL: The Real Cost of OwnershipThe €5 VPS looks cheaper than the €14 managed plan — until you price the hours. A line-by-line cost of ownership for self-hosting Postgres versus managed, the risk nobody puts on the invoice, and when self-hosting actually wins.Jul 27, 2026Read](https://runsite.app/blog/self-hosted-vs-managed-postgresql)[Databases11 min readpgvector: Storing and Searching AI Embeddings in PostgreSQLYour app called an embeddings API and got back 1,536 numbers per document. Before you add a vector database to the stack: what pgvector stores, how its two indexes trade recall for speed, and when Postgres alone carries a semantic search feature.Jul 24, 2026Read](https://runsite.app/blog/pgvector-embeddings-postgresql)[Databases10 min readPoint-in-Time Recovery: How WAL Archiving Saves You After a DROP TABLEYour nightly backup answers "do we have a copy?" Point-in-time recovery answers "can we go back to 3:06pm?" Here's what the write-ahead log stores, how PITR replays it, and what it still can't undo.Jul 22, 2026Read](https://runsite.app/blog/postgres-point-in-time-recovery)[Databases10 min readConnection Pooling with PgBouncer: What Ports 5432 and 6432 Really DoYou switched to port 6432 to stop the connection errors, and now prepared statements throw and SET doesn't stick. That's pooling working as designed. Here's what a pooler actually does, why 5432 and 6432 are different contracts, and what breaks in transaction mode.Jul 20, 2026Read](https://runsite.app/blog/pgbouncer-connection-pooling)[Databases11 min readManaged PostgreSQL for Developers: What the Host Actually Takes Off Your PlateInstalling Postgres takes a minute. Running it in production for three years is the real job. Here's the specific list of operational chores that move to the host when a database is "managed" — and the ones that stay yours no matter what.Jul 17, 2026Read](https://runsite.app/blog/managed-postgresql-for-developers)[Compliance10 min readSchrems II and Data Transfers: Why an EU-Only Stack Is the Stable AnswerSchrems II struck down Privacy Shield in 2020 — the second EU-US data bridge to collapse in five years. Here's what the ruling actually said, why these frameworks keep failing, and why keeping data in the EU sidesteps the entire cycle.Jul 15, 2026Read](https://runsite.app/blog/schrems-ii-data-transfers)[Compliance10 min readEU vs US Hosting: Latency, Law, and CostChoosing between EU and US hosting isn't a contest for the "better" region. It's three separate trade-offs — latency, legal exposure, and cost — and each one points at a different thing. Here's how each axis actually breaks down, and a simple way to pick.Jul 13, 2026Read](https://runsite.app/blog/eu-vs-us-hosting)[Compliance9 min readWhere to Store EU User Data: What GDPR Actually RequiresA customer's security review asks where their data physically lives and whether it leaves the EU. GDPR has no hard rule that personal data must stay in Europe — but every transfer out needs a legal mechanism, and EU residency is the simplest way to skip that problem entirely.Jul 10, 2026Read](https://runsite.app/blog/gdpr-data-residency)[Compliance10 min readData Processing Agreement for SaaS: What Article 28 RequiresA deal stalls because their legal team wants your DPA before signing. Here's what GDPR Article 28 requires inside a Data Processing Agreement, which side of it your SaaS is on, and how to keep it from becoming a bottleneck in your sales cycle.Jul 8, 2026Read](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa)[Storage8 min readS3 Egress Fees: Why Downloading Your Own Data Costs So MuchStorage is the cheap line on an object-storage bill. Egress, the charge for data leaving the bucket, is where the money actually goes. Here's how S3 data-transfer fees work, how to calculate your real bill, and why zero-egress storage changes the math.Jul 6, 2026Read](https://runsite.app/blog/s3-egress-fees)[Compliance11 min readEU Hosting for Developers: Data Residency, GDPR, and DPAs in PracticeA customer asks where their data lives, or a deal stalls on a DPA request, and suddenly "we host in Europe" isn't a good enough answer. Here's what EU hosting actually means for developers: data residency, GDPR roles, DPAs, and how to build a stack that keeps data on EU soil.Jul 3, 2026Read](https://runsite.app/blog/eu-hosting-for-developers)[Databases10 min readPostgreSQL "FATAL: sorry, too many clients already" — What It Means and How to Fix ItYour app is suddenly throwing 500s and the database logs are full of "too many clients already." Here's what the error really means, how to see what's hogging your connections, and the fix that actually holds: pooling, not a bigger number.Jun 26, 2026Read](https://runsite.app/blog/postgres-too-many-clients-already)[Databases7 min readHow to Automate PostgreSQL Backups to S3 on a ScheduleA managed database backs itself up daily — but a second, portable copy in your own S3 bucket is yours to keep. Here's how to schedule pg\_dump to object storage with a cron job, and when it's worth doing.Jun 22, 2026Read](https://runsite.app/blog/postgresql-backup-to-s3)[Deployment7 min readWhat Are Cold Starts — and Why Your Free-Tier App Keeps Falling AsleepCold starts make the first request to a sleeping app painfully slow. Here's why free hosting tiers spin down, what it costs you, and how to keep an app warm without paying for idle capacity.Jun 22, 2026Read](https://runsite.app/blog/cold-starts)

---

# Free Instance Spin Down: Why Requests Take 50+ Seconds

> "Your free instance will spin down with inactivity" is Render's warning. What the 50-second delay is, why free tiers sleep, and how to keep an app warm.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. What Are Cold Starts — and Why Your Free-Tier App Keeps Falling Asleep

DeploymentJune 22, 20267 min read

# What Are Cold Starts — and Why Your Free-Tier App Keeps Falling Asleep

Cold starts make the first request to a sleeping app painfully slow. Here's why free hosting tiers spin down, what it costs you, and how to keep an app warm without paying for idle capacity.

## The short version

A cold start is the delay a request pays when it lands on an app the platform had stopped. Free tiers spin containers down after roughly 15 minutes idle, and waking one costs seconds rather than milliseconds. It is a property of the plan, not of your code — so optimising startup only shaves a floor the platform set.

[Teo Marquardt](https://runsite.app/about#author)· Updated August 21, 2026

You open your side project to show a friend, and the first page just sits there. Eight seconds, maybe more. You refresh and it loads instantly. That gap has a name: a cold start. On most free hosting tiers it isn't a glitch, either. It's how the economics work, and it's worth understanding before you reach for a fix.

## What is a cold start?

A cold start is the delay you pay when a request arrives at an application that isn't currently running. The container holding your app was stopped to free up resources, so before anyone can get a response, the platform has to bring it back to life.

That wake-up is a stack of steps run back to back: schedule the container onto a host, pull or unpack the image, boot the runtime, run your startup code, open database connections, warm whatever caches you keep in memory. That last step is the one a shared cache changes, because [a cache outside the process survives the restart while an in-process one does not](https://runsite.app/blog/cache-aside-write-through-write-behind). None of it is slow on its own. Stacked together, though, they turn a request that should take 50 milliseconds into one that takes several seconds. The next request lands on a container that's already running, so it's quick again, right up until the app goes idle and you're back where you started. That last step is worth pulling out: an in-process cache is empty again after every wake-up, while [a cache that lives outside the container](https://runsite.app/blog/when-to-use-redis-cache) still has its entries, which is one of the quieter reasons teams move one out of application memory.

The database step deserves a note, because it has a failure mode of its own. A container that wakes up opens a fresh connection pool, and the pool belonging to the instance it replaced isn't always closed by then. Scale up and down a few times in quick succession and those overlapping pools can exhaust the database's connection limit, which surfaces as [PostgreSQL's "too many clients already" error](https://runsite.app/blog/postgres-too-many-clients-already) rather than as anything that looks like a cold start.

## Why free tiers put your app to sleep

Keeping a container running costs the provider money whether anyone uses it or not. A free app with one visitor a week would otherwise sit on CPU and memory around the clock for nothing. So most free tiers take that capacity back: after a short stretch of inactivity, usually about 15 minutes, they spin the container down. The next visitor pays for waking it up.

It's a reasonable trade for the provider. The problem is that the inactivity timer is usually blunt: it doesn't distinguish between an app nobody uses and an app that simply had a quiet 20 minutes overnight. Both get put to sleep, and both make their next real visitor wait. Static sites are the exception, since a CDN serving files has nothing to wake up; their free tiers fail differently, by pausing the site or asking for an upgrade when a quota runs out, which is how [European Netlify alternatives](https://runsite.app/blog/netlify-alternatives-europe) are compared.

## "Your free instance will spin down with inactivity, which can delay requests by 50 seconds or more"

That sentence is Render's, shown in its own dashboard next to a free web service, as of writing. It is quoted here because it is the clearest statement any provider makes about the trade, and because the number in it is not marketing pessimism. Fifty seconds is the provider's own estimate of a worst case, and people do search for that exact line after reading it and wondering whether it is normal.

It is normal, and the number decomposes into the steps listed above. Scheduling and image pull dominate on a first wake, the runtime boot is whatever your language costs, and then your own startup code runs: migrations checked, connection pools opened, config fetched, caches filled. A small Go binary can be serving in a second. A Rails or Spring app that opens a database connection and warms an ORM will use most of the fifty. The provider quotes the upper end because it has to cover the slowest app on the platform, not because it is being careful with you.

Two things follow. The delay is paid by a visitor, not by you, so you will rarely see it yourself once you have been clicking around your own app. And it is a property of the plan rather than of your code, so optimising your startup shaves seconds off a number whose floor is set by the platform.

## What a cold start actually costs you

A few seconds sounds harmless until you trace where those seconds land:

* **First impressions.** The one moment you actually share the link, whether it's a client demo, a Show HN post, or a recruiter opening your portfolio, is exactly when the app is most likely to be cold.
* **Webhooks that don't retry.** A payment provider or Git host fires a webhook, your sleeping app takes too long to wake, the sender times out, and the event is silently lost. GitHub, for one, [gives up after 10 seconds and never retries a delivery that timed out](https://runsite.app/blog/github-webhook-timed-out).
* **\[Cron jobs\](https://runsite.app/services/cron-jobs) and health checks.** A scheduled call hits a cold container, exceeds its timeout, and is logged as a failure that never actually had a chance to run.
* **Uptime and SEO crawlers.** Monitoring tools and search bots record the slow first response, dragging down the numbers you're trying to improve.

## Cold starts vs. genuinely idle apps: a false tradeoff

The usual framing is that you either pay for an always-on server or live with cold starts. But that bundles two separate questions into one. Should an app nobody touches keep burning resources? Reasonably, no. Should an app that's actively used ever go cold? Also no. A 15-minute timer can't tell those two apart, so it punishes both.

The distinction that matters

Reclaiming a container that has had zero traffic for weeks is good housekeeping. Sleeping a container that served a request 16 minutes ago is just shifting cost onto your next visitor. A platform can do the first without doing the second.

## How to keep an app warm

### 1\. Ping it yourself

The classic workaround is an external scheduler that hits your app every few minutes so the inactivity timer never fires:

bash

```
# A cron entry that pokes the app every 10 minutes
*/10 * * * * curl -fsS https://my-app.example.com/health > /dev/null
```

It works, but it's a band-aid. You're burning compute around the clock to fake activity, the keep-alive ping itself counts toward any request or bandwidth limits, and several providers explicitly discourage or block it. You've rebuilt always-on, just less reliably and against the grain of the platform.

### 2\. Pay for an always-on instance

Upgrading to a paid plan removes the inactivity sleep entirely. The container keeps running on dedicated resources, which is the right call for anything in production. The catch is that it's overkill for the long tail of small but real projects that barely touch the CPU yet still shouldn't greet a visitor with an eight-second wait.

### 3\. Use a platform that doesn't sleep active apps

The cleanest fix is to not make the tradeoff at all. On [Runsite Web Services](https://runsite.app/services/web-services), free apps stay warm as long as they're getting traffic, so an active app never cold-starts. A container is only suspended after 14 full days without a single request, and the next visit wakes it back up with your code, environment variables, and storage intact. So any live demo, bot, or side project with even the occasional visitor never goes cold. Only the genuinely abandoned ones get reclaimed. The image it wakes up from is whatever your last deploy produced, which is a mechanism worth knowing in its own right: [what actually happens between git push and live traffic](https://runsite.app/blog/git-push-to-deploy).

Want a hard guarantee that an app never sleeps, even after weeks of total silence? Starter and above, [from €5/mo](https://runsite.app/pricing), skip the inactivity window completely. Everything runs in the EU (Germany) with a signed [GDPR data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) on every plan, so you're not trading responsiveness for data residency to get it.

## When cold starts actually matter

Plenty of workloads don't care. A nightly batch job can swallow a few seconds of startup and nobody will ever notice, as long as the job runs outside the app; [a scheduler inside a container that has gone to sleep never fires at all](https://runsite.app/blog/cron-job-multiple-instances). But anything a person is waiting on, anything another system calls with a timeout attached, anything you'll be judged on the first time someone clicks the link, that needs to stay warm. Staying warm and paying for idle capacity were never actually the same thing. You just need a platform that can tell a quiet app from a dead one.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does "your free instance will spin down with inactivity" mean?

It is the notice Render shows in its dashboard beside a free web service, and the full sentence continues "which can delay requests by 50 seconds or more". It means the platform stops your container after a stretch with no traffic, usually around 15 minutes, and starts it again when the next request arrives. That restart is the cold start: the container has to be scheduled onto a host, the image unpacked, the runtime booted and your own startup code run before anything can be served. Fifty seconds is the upper end the provider quotes to cover the slowest applications on the platform; a small compiled binary often wakes in about a second, while a framework that opens database connections and warms an ORM on boot will use much more of it. The delay lands on whoever arrives first, not on you, and it is a property of the free plan rather than of your code, so tuning your own startup only moves part of the number.

### Do cold starts affect paid plans?

As a rule, no. Paid plans run with dedicated resources and skip the inactivity sleep entirely, so the container stays running regardless of traffic. On Runsite the one exception is the €3 Nano entry plan, which suspends a service only after 14 consecutive days with no requests. Cold starts on free tiers come from the platform reclaiming idle capacity — paid instances aren't reclaimed.

### Is pinging my app a good way to keep it warm?

It works, but it's a workaround. A keep-alive ping burns compute around the clock to simulate traffic, counts against request and bandwidth limits, and is discouraged by some providers. A platform that doesn't sleep active apps removes the need for it.

### How long until a Runsite app sleeps?

A Runsite web service on the entry Nano plan (€3/mo) stays warm as long as it receives traffic — there are no cold starts on active apps. A container is only suspended after 14 consecutive days with zero requests, and the next request wakes it back up. Starter and above never sleep on inactivity.

Keep reading

---

# Automate PostgreSQL Backups to S3 on a Schedule

> A managed database backs itself up daily, but a portable copy in your own S3 bucket is yours to keep. How to schedule pg_dump to object storage with cron.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. How to Automate PostgreSQL Backups to S3 on a Schedule

DatabasesJune 22, 20267 min read

# How to Automate PostgreSQL Backups to S3 on a Schedule

A managed database backs itself up daily — but a second, portable copy in your own S3 bucket is yours to keep. Here's how to schedule pg\_dump to object storage with a cron job, and when it's worth doing.

## The short version

Your managed database already backs itself up. A scheduled pg\_dump to your own S3 bucket adds a second, portable copy that does not live in your provider's account. It is one script and one cron job, and it is worth doing when you need portability, a longer retention window, or a restore path you control.

[Teo Marquardt](https://runsite.app/about#author)

Your managed database already backs itself up every night. So why write a script to copy it somewhere else? For a lot of projects the honest answer is that you don't have to. But there's a specific kind of failure that a provider's own backups can't save you from, and the fix is one scheduled job. This is about when a second copy earns its keep, and how to set one up without turning it into a weekend project.

## Doesn't a managed database already back itself up?

It does, and it does it well. [Runsite's managed PostgreSQL](https://runsite.app/services/postgresql) takes a daily automated backup, keeps it for up to 30 days, and supports [point-in-time recovery](https://runsite.app/blog/postgres-point-in-time-recovery), so you can roll the database back to any second inside that window. For everyday accidents like a bad migration, a DELETE that forgot its WHERE clause, or the table someone dropped at 2am, that's exactly what you want, and it's already running.

The gap isn't quality. It's that every one of those copies lives in the same place: same provider, same account, same 30-day window. That's a single point of failure dressed up as a backup. The old sysadmin answer to this is the 3-2-1 rule: keep three copies of your data, on two kinds of storage, with one of them somewhere else entirely. A provider snapshot covers the first copy. A dump in your own bucket covers the rest.

Here are a few situations where that second copy stops being paranoia:

* **You need to keep backups longer than 30 days.** Retention caps out at a month. If an audit, a compliance rule, or your own archive policy says "keep a yearly snapshot for seven years," the provider window can't do that on its own.
* **You want a copy you can take with you.** A pg\_dump file in a bucket you control isn't tied to any platform. You can restore it to a laptop, another host, or a different provider without asking anyone's permission. It's an exit plan you hope to never use and are glad to have.
* **You want disaster recovery beyond the provider.** Account lockout, a billing dispute, a region-wide incident: rare, but if your only backups live inside the account you just lost access to, they may as well not exist. A separate bucket, ideally a separate account, is cheap insurance.

## What a good automated backup actually needs

Before any code, it helps to know what separates a real backup routine from a pg\_dump someone ran once and forgot. A few things have to be true:

* It runs on a schedule, without anyone remembering to trigger it.
* It's compressed, because a raw SQL dump of a real database is large and mostly text.
* Each file has a unique, date-stamped name, so a new backup never overwrites yesterday's.
* It fails loudly. A backup job that silently stops working is worse than no backup, because you'll trust it right up until you need it.
* Old copies get cleaned up on a lifecycle policy, so storage doesn't grow forever.

## The backup script

The core of it is three commands chained together: dump the database, compress the output, and upload it to object storage.

bash

```
#!/bin/bash
set -euo pipefail

# Dump, compress, and stream straight to a date-stamped object
pg_dump "$DATABASE_URL" | gzip | \
  aws s3 cp - "s3://my-backups/postgres/$(date +%F).sql.gz"
```

Reading it left to right: [pg\_dump](https://www.postgresql.org/docs/current/app-pgdump.html) reads the whole database from the connection string in $DATABASE\_URL and writes plain SQL to standard output. gzip compresses that stream on the fly. The aws s3 cp - command takes the compressed stream and uploads it as a single object, named with the current date so each run lands in its own file. The set -euo pipefail line at the top matters more than it looks. Without it, if pg\_dump fails halfway, the script can still upload a truncated file and exit as if nothing went wrong.

One detail trips people up: if your bucket is S3-compatible storage rather than AWS itself (Runsite's is), the CLI needs an `--endpoint-url` flag so it talks to the right host instead of guessing an AWS region. The exact value is in your storage settings. Same command, one extra flag.

Keep credentials out of the command

Store the connection string and your storage keys in environment variables, not in the script itself. Anything printed on the command line can end up in logs, and a database URL with a password in it is not something you want sitting in plaintext.

## Scheduling it as a cron job

A script you have to run by hand isn't automation. The job needs something to trigger it every day, on time, whether or not your laptop is on. That's what [managed cron jobs](https://runsite.app/services/cron-jobs) are for: you give it a schedule and a container, and it runs the job for you. A schedule is the right shape here because the work has a time and nobody is waiting for it; [when a cron job is the wrong shape and you need a worker or a queue instead](https://runsite.app/blog/cron-jobs-workers-queues) is the broader version of that decision.

Since each run executes a Docker container, the image you point it at needs pg\_dump and the aws CLI available inside it. The official Postgres image with the AWS CLI added covers it, or a small purpose-built image if you'd rather keep it lean. Whatever you pick, match the Postgres client version to your database so the dump format lines up.

A daily backup at 2am is a standard cron expression:

bash

```
# At 02:00 every day
0 2 * * *
```

A few settings turn this from "runs the script" into "runs the script safely":

* **Timezone.** Pin the schedule to a timezone so 2am means 2am where you expect, not wherever the server happens to think it is.
* **A timeout.** A backup that hangs shouldn't run forever. Give it a generous ceiling and let the platform kill it if it blows past.
* **Concurrency set to Forbid.** If last night's backup is somehow still running when tonight's is due, you don't want two pg\_dumps fighting over the same database. Forbid skips the new run instead of piling on. On Runsite that's the default.
* **A failure alert.** Wire up an email or webhook notification on a non-zero exit code. This is the part that makes the whole thing trustworthy: you find out the night it breaks, not the morning you need to restore.

Runsite's cron jobs keep a history of every run with its exit code and duration, so you can glance at the log and confirm last night's backup actually finished, not just that it started.

## Where the copy lives

The destination is an [S3-compatible bucket](https://runsite.app/services/s3-storage) you own, kept separate from the provider's own snapshots. That separation is the whole point: a separate copy, somewhere else, doing a different job from the managed backup. If buckets are new to you, or you're weighing up which provider to keep the copy with, [how S3-compatible object storage works](https://runsite.app/blog/s3-compatible-object-storage) covers the model and the things worth checking before you commit data to one.

Set a lifecycle rule on the bucket to match your retention policy. Maybe daily backups expire after 90 days while the first-of-the-month copy is kept for a year. The bucket handles the cleanup, so the script never has to.

Worth a thought while you're here: encryption. Runsite's object storage encrypts every object at rest with AES-256, so the dump isn't sitting in the clear once it lands. If you want it encrypted before it ever leaves the job, pipe the stream through gpg in the script and hold the key yourself, which keeps the backup readable only to you even if the bucket is ever exposed.

If your database, cron job, and bucket all sit in the same region, the backup never leaves it. On Runsite, [PostgreSQL](https://runsite.app/services/postgresql), Cron Jobs, and object storage all run in the EU (Germany), so the dump travels between services in one region instead of crossing the open internet between three different providers.

## Restoring from it

A backup you've never restored isn't a backup, it's a hope. Test the round trip at least once so you know the file is good and you know the steps before you need them in a hurry.

Restoring reverses the script: pull the object down, decompress it, and pipe it into psql.

bash

```
aws s3 cp "s3://my-backups/postgres/2026-06-22.sql.gz" - | \
  gunzip | psql "$TARGET_DATABASE_URL"
```

The usual snag is role ownership: restore into a database with different roles than the original and psql will complain about missing owners. Adding `--no-owner` to the dump command sidesteps it by leaving ownership to whoever runs the import. That one flag aside, the full process, including restoring into a fresh database and the odd large-object edge case, is worth reading once in the [Runsite docs](https://docs.runsite.app) rather than improvising during an incident.

## When a second copy is worth it

Not every project needs this. If you're running a side project or an early-stage app, the managed daily backups with 30-day retention and point-in-time recovery are genuinely enough, and adding a second pipeline is effort you could spend elsewhere.

Reach for your own copy when one of these sounds like you:

* You're in a regulated space and 30 days of retention won't satisfy an auditor.
* You'd sleep better knowing you could walk to another provider tomorrow with your data in hand.
* Losing access to your provider account would be an extinction event, not an inconvenience.
* You've already lived through one bad restore and have no intention of repeating it.

The good news is that the whole thing is one script and one scheduled job. You're not building a backup system from scratch; you're adding a second, portable copy on top of the one you already have. [Schedule your first backup job](https://runsite.app/services/cron-jobs) and keep a copy that's yours to take anywhere.

Related service

## Cron Jobs on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Cron Jobs](https://runsite.app/services/cron-jobs)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Isn't a managed database backup enough?

For most projects, yes. Managed PostgreSQL on Runsite runs daily backups with up to 30 days of retention plus point-in-time recovery, which covers the common failures like a bad migration or a dropped table. A second copy in your own bucket is worth adding when you need longer retention, a portable copy you can restore anywhere, or disaster recovery that survives losing access to the provider account.

### How often should I back up Postgres to S3?

A daily backup is the common default and fits most applications. If your data changes constantly and you can't afford to lose a day, lean on point-in-time recovery for fine-grained rollback and use the S3 dump as your longer-term, off-site copy. Match the frequency to how much data you're willing to lose, not to a habit.

### How do I restore from an S3 backup?

Download the object, decompress it, and pipe it into psql against the target database: aws s3 cp s3://.../backup.sql.gz - | gunzip | psql $DATABASE\_URL. Test the restore at least once before you rely on it, since a backup you've never restored is an untested assumption. The full process is documented in the Runsite docs.

Keep reading

---

# PostgreSQL "FATAL: sorry, too many clients already" Fix

> Every max_connections slot is taken. Find what holds them with pg_stat_activity, then fix it with pooling on port 6432, not a bigger number.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. PostgreSQL "FATAL: sorry, too many clients already" — What It Means and How to Fix It

DatabasesJune 26, 202610 min read

# PostgreSQL "FATAL: sorry, too many clients already" — What It Means and How to Fix It

Your app is suddenly throwing 500s and the database logs are full of "too many clients already." Here's what the error really means, how to see what's hogging your connections, and the fix that actually holds: pooling, not a bigger number.

## The short version

The error counts connections, not load, which is why a bigger instance rarely fixes it. Read pg\_stat\_activity to tell real concurrency from idle leftovers and stuck transactions. Pool inside each app, put PgBouncer in front of the fleet, and raise max\_connections last — and only with the memory to match, since every connection forks a backend process.

[Teo Marquardt](https://runsite.app/about#author)· Updated September 29, 2026

A deploy goes out, traffic picks up, and your app starts throwing 500s. You open the logs and there it is, over and over: "FATAL: sorry, too many clients already." Nothing changed in your queries, the database is barely breaking a sweat on CPU, and yet new requests can't get in. The error reads like the database is overloaded. It almost never is. What's actually full is the connection pool, and once you see why, the fix is straightforward and permanent.

## What the error actually means

PostgreSQL accepts a fixed number of simultaneous client connections, set by the [max\_connections parameter](https://www.postgresql.org/docs/current/runtime-config-connection.html) (100 by default). When connection number 101 tries to open while the first 100 are still held, the server doesn't queue it or slow it down. It refuses outright with this error. You'll sometimes see its sibling, "remaining connection slots are reserved for non-replication superuser connections," which is the same wall hit a few slots earlier, because Postgres keeps a handful in reserve so an admin can still log in to fix the mess. If your log says `ECONNREFUSED` instead, Postgres never got that far: nothing was listening on the port, and [the address in that error is the clue](https://runsite.app/blog/econnrefused-127-0-0-1).

The key thing: this is a count of open connections, not a measure of load. An app running ten trivial `SELECT 1` queries can exhaust the limit while one running a single heavy report stays well under it. The error is about how many doors are open, not how much work is going through them.

## Why Postgres has a connection ceiling at all

It helps to know why the limit exists, because it explains why "just raise it" is a trap. PostgreSQL uses a process-per-connection model: every connection forks a dedicated backend process on the server, and each one reserves memory for its work area, query plans, and buffers, idle or not. A hundred connections doing nothing still cost real RAM. Push `max_connections` to 500 on a small instance and you can starve the database of the memory it needs to actually run queries, trading a clean refusal for thrashing and OOM kills. The ceiling isn't arbitrary; it's the database protecting itself.

## Find out what's eating your connections

Before changing anything, look at who's actually connected. Postgres exposes this in the [pg\_stat\_activity view](https://www.postgresql.org/docs/current/monitoring-stats.html). Group it by state to get the shape of the problem in one query:

sql

```
-- How many connections, and what are they doing?
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count(*) DESC;
```

The state column is the tell, and each value points at a different fix:

* **`active`** — real concurrent work in flight. The answer is pooling, so fewer connections do the same amount of work.
* **`idle`** — your app opened the connection and walked away without closing it. The answer is fixing how it manages connections.
* **`idle in transaction`** — the worst of the three: a connection that ran a query, never committed or rolled back, and is now holding both a slot and its locks hostage. A few of these can wedge a whole app.

The usual culprit: connections multiply with instances

If each app instance opens its own pool of, say, 20 connections, then four instances plus a couple of background workers and a cron job quietly add up to well past 100, even though no single piece looks unreasonable. Connection math is per-fleet, not per-process. Count [every instance, worker, and scheduled job](https://runsite.app/blog/cron-jobs-workers-queues) that talks to the database.

## The real fix: pool your connections

Opening a fresh connection per request is the root cause behind most of these incidents. Connections are expensive to create and strictly limited, so the fix is to open a small set once and reuse them. There are two layers to do it at, and busy apps want both.

### 1\. Pool inside your app

Every serious database driver ships a pool. Use it, and cap it deliberately instead of leaving it on a generous default. The cap is per process, so multiply it by how many instances you run and keep the total comfortably under `max_connections`:

javascript

```
import { Pool } from 'pg';

// One pool per process, reused across requests.
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10, // ceiling PER instance: 10 x instances must stay under max_connections
});

// Borrow and return; never open a connection per request.
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
```

This alone resolves a surprising share of "too many clients" fires. The trap to avoid is creating the pool inside a request handler, which spins up a brand-new pool on every call and reproduces the exact problem you're trying to kill. Create it once, at startup.

### 2\. Put a pooler in front of the database

App-side pools have a hard limit: they can't coordinate across instances. Each one only knows its own connections, so a fleet that scales horizontally will still blow the ceiling no matter how careful each process is. The fix is a single pooler that sits between your apps and Postgres, multiplexing thousands of client connections down onto a small set of real database ones. PgBouncer is the standard tool for this, and in transaction-pooling mode it hands a real connection to a client only for the duration of a transaction, then returns it to the shared set, so a handful of backend connections can serve a large fleet.

Running PgBouncer yourself means another process to deploy, configure, and keep alive. [Runsite's managed PostgreSQL](https://runsite.app/services/postgresql) has PgBouncer built in, so there's no separate pooler to operate. You connect to the standard port 5432 for a direct session, or to port 6432 for the pooled endpoint, controlled by a single flag on the connection string:

bash

```
# Direct connection (port 5432)
DATABASE_URL=postgresql://user:pass@db.runsite.app:5432/mydb

# Pooled through PgBouncer (port 6432)
DATABASE_URL=postgresql://user:pass@db.runsite.app:6432/mydb?pgbouncer=true
```

One caveat worth knowing up front: transaction pooling doesn't play nicely with session-level features like prepared statements or `SET` that expect to live for a whole session. Most apps and ORMs have a setting for this; the specifics for each driver are in the [Runsite docs](https://docs.runsite.app) rather than something to improvise mid-incident. For the fuller picture of the pooling modes and what the two ports actually promise, see [connection pooling with PgBouncer](https://runsite.app/blog/pgbouncer-connection-pooling).

## Hunt down leaked and idle connections

If `pg_stat_activity` showed a wall of `idle` or `idle in transaction` rows, pooling alone won't save you, because something is opening connections and never letting go. The common causes are short: a code path that opens a connection and returns before closing it, a transaction that errors out without a rollback, or a worker that holds a connection open while it waits on something slow.

To buy breathing room in the moment, you can terminate the connections that have been sitting idle the longest:

sql

```
-- Free connections idle for more than 10 minutes
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
  AND state_change < now() - interval '10 minutes'
  AND pid <> pg_backend_pid();
```

That's a fire extinguisher, not a fix; the leak will refill the slots if you don't find it. The durable version is to let the database reap them automatically by setting `idle_in_transaction_session_timeout`, so a forgotten transaction can't hold a slot indefinitely. Pair that with the pooling above and the idle pile stops coming back.

## Where this error usually comes from

The error text is identical everywhere, but the thing filling the slots depends on what you're running. Four setups account for most of the cases, and each has a different tell.

### Docker and Docker Compose

A Postgres container ships with the stock `max_connections` of 100, and Compose makes it easy to outgrow that without noticing. `docker compose up --scale api=4` quadruples your pools while the database config stays where it was. Restarting an app container makes it worse before it makes it better: the old backends aren't closed cleanly, so Postgres keeps them until TCP keepalive expires, and the fresh container opens a full pool on top of them. If the count drops on its own a few minutes after a restart, that's what you were looking at — the same overlap that makes [cold starts](https://runsite.app/blog/cold-starts) briefly expensive on platforms that scale containers up and down. Set the pool size per container from the fleet total, not per service, and raise the container's `max_connections` deliberately rather than letting the default decide.

### Django

Django's `CONN_MAX_AGE` is the usual cause. At the default of `0` every request closes its connection, which is wasteful but safe. Set it to a non-zero value (or `None`) without a pooler in front and every Gunicorn worker holds a connection open for its lifetime. The arithmetic is workers times instances: 16 workers across three instances is 48 permanent connections before Celery or a management command touches the database. Either keep `CONN_MAX_AGE` low and let a pooler do the reuse, or keep persistent connections and size the worker count against `max_connections` on purpose. On Django 4.2 and later, turn on `conn_health_checks` so a recycled connection that died server-side doesn't surface as an error in the request.

### DBeaver, pgAdmin, and other GUI clients

A single connection in a database IDE is rarely a single connection to Postgres. DBeaver opens a separate session for metadata, and typically another per SQL editor tab, and holds all of them for as long as the app is open. Three developers with a handful of tabs each can quietly occupy a fifth of a 100-connection limit while doing nothing at all. If `pg_stat_activity` shows `idle` rows whose `application_name` is a client tool, that's your answer. Close unused editors, or point the tools at a pooled endpoint.

### A local Postgres on Ubuntu or Debian

The package default is the same 100, set in `/etc/postgresql/<version>/main/postgresql.conf`. Changing it needs a full `sudo systemctl restart postgresql`, not a reload, because `max_connections` is allocated at startup. On a dev machine the culprit is usually simpler than a config value: `psql` sessions left in old terminal tabs, plus every project's dev server holding its own pool against the same instance.

## When raising max\_connections is (and isn't) the answer

Sometimes the limit really is too low for legitimate, well-pooled traffic, and the right move is genuinely a bigger instance. The order matters, though. Pool first, find any leaks second, and only then raise the ceiling, because each new connection costs memory and a higher limit on the same small instance buys instability, not headroom. There is a fourth option ahead of all three, which is that some of those queries should never reach the database at all, and [deciding when a Redis cache is the right answer](https://runsite.app/blog/when-to-use-redis-cache) is a separate question from how many connections you can afford.

When you do need more, raise the resources, not just the number. Whether that's worth paying for rather than tuning yourself is the whole [self-hosted vs managed PostgreSQL](https://runsite.app/blog/self-hosted-vs-managed-postgresql) question, priced line by line. On Runsite, [managed PostgreSQL](https://runsite.app/services/postgresql) scales vertically with one click, which lifts the memory the extra connections actually need along with the limit. And because the database, its WAL archive, and every backup stay in the EU (Germany) with a signed [GDPR data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) on every plan, scaling up doesn't quietly move your data somewhere it shouldn't be.

## The short version

"Too many clients already" is a connection-count problem wearing the costume of an overload. Check `pg_stat_activity` to see whether you're looking at real concurrency, idle leftovers, or stuck transactions. Pool connections inside each app and put PgBouncer in front of the fleet so thousands of clients ride on a handful of real connections. Reach for a bigger `max_connections` last, and when you do, give it the memory to match. Do that and the error stops being a recurring incident and becomes a line you never see again. [Spin up a managed PostgreSQL database](https://runsite.app/services/postgresql) with pooling already handled, and skip the part where you operate it yourself.

Related service

## Managed PostgreSQL on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed PostgreSQL](https://runsite.app/services/postgresql)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does "too many clients already" mean in PostgreSQL?

It means the number of open connections has reached max\_connections (100 by default), so PostgreSQL refuses any new connection until an existing one closes. It's a count of open connections, not a measure of database load. An app can hit the limit with trivial queries while CPU stays near idle. The fix is almost always connection pooling, not a bigger database.

### Should I just increase max\_connections?

Rarely as a first step. Every connection in PostgreSQL forks a backend process that reserves memory whether it's busy or not, so a high max\_connections on a small instance can starve the database and cause instability. Pool your connections first, fix any leaks (idle or idle-in-transaction sessions), and only then raise the limit, raising the instance resources alongside it, not just the number.

### What's the difference between port 5432 and 6432 on Runsite?

Port 5432 is a direct connection to PostgreSQL; port 6432 routes through the built-in PgBouncer pooler. Use the pooled 6432 endpoint (with ?pgbouncer=true) for high-concurrency app traffic so many clients share a small set of real database connections. Use the direct 5432 endpoint for migrations or admin tasks that need session-level features like prepared statements.

### Why do I get "too many clients already" in Docker Compose?

A Postgres container uses the stock max\_connections of 100, and scaling app services multiplies pools against it — four replicas with a pool of 20 each is already 80 connections. Restarting an app container also leaves its old backends open until TCP keepalive expires, so the fresh container's pool stacks on top of them for a few minutes. Size pools from the fleet total rather than per service, and set max\_connections on the container explicitly.

### How do I fix "too many clients already" in Django?

Check CONN\_MAX\_AGE. Any non-zero value makes each Gunicorn worker hold a connection for its lifetime, so the connection count is workers times instances — 16 workers across three instances is 48 permanent connections before Celery is counted. Either keep CONN\_MAX\_AGE at 0 and let a pooler handle reuse, or keep persistent connections and size worker count against max\_connections deliberately. On Django 4.2+, enable conn\_health\_checks so recycled dead connections don't surface as request errors.

### Why does DBeaver cause "too many clients already"?

Database IDEs don't open one connection per database. DBeaver keeps a separate session for metadata and usually another for each SQL editor tab, holding all of them while the app is open. A few developers with several tabs each can occupy a large share of a 100-connection limit while running no queries. Look for idle rows in pg\_stat\_activity whose application\_name is a client tool, then close unused editors or connect the tools through a pooled endpoint.

Keep reading

---

# EU Hosting for Developers: Data Residency, GDPR & DPAs

> What EU hosting really means for developers: data residency vs sovereignty, controller vs processor, why you need a DPA, and how to keep your stack in the EU.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. EU Hosting for Developers: Data Residency, GDPR, and DPAs in Practice

ComplianceJuly 3, 202611 min read

# EU Hosting for Developers: Data Residency, GDPR, and DPAs in Practice

A customer asks where their data lives, or a deal stalls on a DPA request, and suddenly "we host in Europe" isn't a good enough answer. Here's what EU hosting actually means for developers: data residency, GDPR roles, DPAs, and how to build a stack that keeps data on EU soil.

## The short version

EU hosting is not a region dropdown. It means every copy — runtime, database, backups, storage, caches, logs, email — stays inside the EU. Under GDPR you are the controller and your host the processor, so a signed DPA is mandatory rather than optional. The manageable version is one EU region by default instead of per-service policing.

[Teo Marquardt](https://runsite.app/about#author)· Updated August 8, 2026

It usually starts with a single question. A prospective customer, halfway through a security review, asks where their users' data is stored. Or a deal you thought was closed goes quiet because their legal team wants a Data Processing Agreement before signing. Or you read the words "data controller" in a support ticket and realize you're not entirely sure which one you are. Up to that point, "we host in Europe" felt like enough. It rarely is, and the gap between that phrase and what it actually requires is where a lot of small teams get caught.

This is the practical version, written for the person who has to configure the infrastructure rather than the person who writes the privacy policy. It covers what EU hosting really means, the GDPR concepts you can't avoid as a developer, why a DPA matters and shouldn't cost you a sales call, and how to assemble a stack that keeps data on EU soil without surprises. None of it is legal advice, but all of it is the groundwork that makes the legal advice cheap.

## What "EU hosting" actually means (and what it doesn't)

The phrase gets used loosely, so it's worth pinning down. EU hosting means your data is physically stored and processed on servers located inside the European Union, and that it stays there. That second half is the part people skip. A provider that lets you pick a Frankfurt region for your primary database has met the first half. If that same provider replicates backups to Virginia for durability, caches responses at edge nodes outside the EU, or routes logs through a US analytics pipeline, your data has left the EU regardless of where the main server sits.

So the useful test isn't "can I choose an EU region?" Almost every large platform offers one. It's "does every copy of my data, including backups, logs, and derived data, stay in the EU by default, without me having to audit each feature?" A US-headquartered platform with an EU region has bolted European hosting onto a fundamentally global architecture. It can work, but the burden is on you to find every place data might leak out and lock it down. An EU-native platform inverts that: EU residency is the default and staying inside the border is the path of least resistance, not the thing you have to fight for.

## Data residency, data sovereignty, and why the difference matters

Two terms get used interchangeably and shouldn't be. Data residency is about location: where the bytes physically live. Data sovereignty is about jurisdiction: whose laws can reach those bytes. You can have the first without the second, and that gap is the whole reason this topic is more than a checkbox.

Here's the concrete version. A US-owned cloud provider stores your data in its Frankfurt region. Residency: satisfied, the data sits in Germany. But under the US CLOUD Act, a US company can be compelled to hand over data it controls no matter where in the world that data sits. So the data resides in the EU while remaining reachable by a non-EU jurisdiction. That's residency without sovereignty, and it's exactly the tension the [Schrems II ruling](https://runsite.app/blog/schrems-ii-data-transfers) put a spotlight on when it struck down the Privacy Shield framework in 2020.

None of this makes a US-owned provider illegal to use. Transfers like these are meant to be covered by a legal mechanism, today the [EU-US Data Privacy Framework](https://commission.europa.eu/law/law-topic/data-protection/international-dimension-data-protection/eu-us-data-transfers%5Fen) and Standard Contractual Clauses before it, and plenty of compliant companies rely on exactly that. The catch is that this is the same class of mechanism that has already been struck down twice, first Safe Harbor, then Privacy Shield, so a stack built on it depends on an adequacy decision that has a history of not holding. Keeping data on infrastructure that's both located in and operated within the EU sidesteps the question: there's no cross-border transfer to justify in the first place, and one fewer legal assumption that can change under you.

Why this shows up in your sales cycle

For most small SaaS products, the practical consequence isn't a court order. It's that enterprise and public-sector buyers increasingly ask about it. "Is your infrastructure operated by an EU entity, or a US company with EU servers?" is now a standard line on security questionnaires. Being able to answer it cleanly removes a category of friction from deals you'd otherwise have to escalate to a lawyer to close.

## GDPR in one page, for developers

You don't need to memorize the regulation, but you do need two concepts, because they decide what your obligations are and who carries them.

The first is the split between a data controller and a data processor. The controller decides why and how personal data is processed; the processor acts on the controller's instructions. If you run a SaaS app that collects user data, you are the controller for your users' data. Your hosting provider, which stores and processes that data on your behalf, is your processor. This isn't a label you get to choose; it follows from what you actually do, and it matters because GDPR assigns different duties to each. As the controller, you're accountable for the whole chain, including your processors.

The second concept is personal data itself, which is broader than most developers assume. It's not just names and emails. IP addresses, cookie identifiers, device IDs, and user-generated content all count. That matters because personal data hides in places you don't think of as a "database": your application logs, your error-tracking tool, your email delivery records, your object storage, even the [AI embeddings you compute from user content](https://runsite.app/blog/pgvector-embeddings-postgresql). If a system touches any of those, it's in scope, and "the main database is in the EU" doesn't cover it.

* **You (the app) are the controller** — accountable for lawful basis, user rights, and choosing compliant processors.
* **Your host is a processor** — must process data only on your instructions and protect it, formalized through a contract.
* **Personal data is everywhere** — logs, backups, caches, email records, and uploads all count, not just the users table.

## The DPA: the contract that makes your host legal to use

A Data Processing Agreement is the contract that binds your processor to GDPR's requirements. [Article 28 of the GDPR](https://eur-lex.europa.eu/eli/reg/2016/679/art%5F28) is explicit: if you use a processor, the relationship must be governed by a contract that sets out what data is processed, for what purpose, for how long, and the security measures around it. In plain terms, if your hosting provider handles personal data for you and there's no DPA in place, you, as the controller, are not compliant. That isn't optional paperwork. The contract is what makes using the provider allowed at all.

The eight clauses Article 28 actually requires, and how to tell which side of the agreement your product signs, are broken down in the guide to [what a data processing agreement must contain](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa). This is where a lot of developer-focused platforms quietly fail their smaller customers. The DPA exists, but it's gated behind an enterprise plan, a sales conversation, or a manual request that takes a week. For a solo founder or a small team, that turns a legal necessity into a procurement obstacle, and plenty of people ship without one simply because getting it was too much friction. That's the wrong default. A DPA costs the provider nothing to include, and every customer handling EU data needs one, so it should come standard.

On Runsite, a signed GDPR Data Processing Agreement is included on every plan: no upgrade, no negotiation, no sales call. It's part of the account by default, which means the moment you need to answer that security-review question, the answer is already in place rather than a two-week detour.

## What actually has to stay in the EU

"Keep data in the EU" is easy to say and easy to violate in pieces, because a real application is more than one service and each piece stores data somewhere. Here's the full surface area to account for, because a stack is only as EU-resident as its leakiest component:

* **Application runtime** — the containers running your code, plus their environment variables and in-memory state. Personal data flows through here on every request.
* **The database** — the obvious one, but with a catch: its backups and point-in-time-recovery snapshots have to stay in-region too, and they're frequently the part that leaks.
* **Object storage** — user uploads, exports, and generated files, which almost always contain personal data.
* **Caches and sessions** — a session store holds tokens and user identifiers; a cache can hold anything you put in it.
* **Logs and derived data** — access logs with IP addresses, error traces with request payloads, email delivery records. Personal data by GDPR's definition, and easy to forget.

## Building an EU-only stack

The clean way to handle all of that is to run the whole stack in one EU region on infrastructure operated by an EU entity, so residency is the default for every layer rather than a setting you toggle per service. On Runsite everything runs in Germany, and nothing falls back to non-EU infrastructure. Here's how the pieces map to the surface area above.

Your [web services](https://runsite.app/services/web-services) and [static sites](https://runsite.app/services/static-sites) run their containers and store their build artifacts, environment variables, and logs in the EU. That covers more ground than it sounds like, because a deploy pipeline produces artifacts and logs of its own, and [everything that happens between a git push and live traffic](https://runsite.app/blog/git-push-to-deploy) happens in the same region. For static sites, public assets are cached at edge locations worldwide for speed, but the source of truth (your build output) stays in Germany. The edge holds public files, not your private data.

Your [managed PostgreSQL](https://runsite.app/services/postgresql) keeps the database, its WAL archive, and every automated backup in the same region, so a point-in-time restore never pulls data across a non-EU boundary. The backup leak described above simply can't happen here. [Redis](https://runsite.app/services/redis) holds your cached data and user sessions in the EU, which matters precisely because session stores are full of tokens and identifiers. [Object storage](https://runsite.app/services/s3-storage) keeps every uploaded object in Germany, encrypted at rest with AES-256, with no replication to non-EU regions.

The pieces that run on their own schedule stay inside the border too. [Cron jobs](https://runsite.app/services/cron-jobs) execute in the EU, so whatever rows or files they touch are processed there, and their logs are stored there. Even [transactional email](https://runsite.app/services/email), which quietly carries personal data in every authentication and billing message, is accepted, signed, queued, and delivered from the EU, with message bodies retained only briefly for troubleshooting. Assemble those and you have a stack where the honest answer to "where does our data live?" is a single region, not a spreadsheet of per-service exceptions.

## The traps that leak data out of the EU

Most residency failures aren't dramatic. They're a default setting nobody changed, on a platform designed for a global footprint. These are the ones worth checking on any stack:

* **Backups in a different region.** The database is in the EU; its backups replicate to a US region for durability. This is the single most common leak, because it's a default that's invisible until you go looking.
* **Edge caches holding private responses.** A CDN that caches personalized API responses — not just public assets — is storing personal data at edge nodes that may sit anywhere in the world.
* **Third-party subprocessors.** Your error tracker, analytics, or email sender may forward data to the US. Each one is a subprocessor you're responsible for, even though it's not "your" server.
* **A US parent company.** Even with EU servers, a US-owned provider is reachable under the CLOUD Act — the residency-without-sovereignty gap. For some buyers this alone is disqualifying.
* **Logs shipped elsewhere.** Access and error logs full of IP addresses routed to a non-EU logging service is personal data crossing the border through the back door.

## The short version

EU hosting means more than a region dropdown. It's the guarantee that every copy of your data (runtime, database, backups, storage, caches, logs, email) stays inside the EU, on infrastructure a non-EU jurisdiction can't reach into. Under GDPR you're the controller and your host is your processor, which means you need a DPA in place and you're accountable for every place personal data lands. The way to make that manageable is to stop treating residency as a per-service setting to police and run the whole stack in one EU region by default, with the DPA already signed. [Deploy your app on EU infrastructure](https://runsite.app/services/web-services) that keeps data in Germany with a GDPR DPA on every plan, and the next time someone asks where your data lives, you'll have a one-word answer.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is hosting in the EU enough to be GDPR-compliant?

No. EU hosting handles data residency (keeping data on EU servers), which is one important piece, but GDPR compliance is broader. You still need a lawful basis for processing, a way to honor user rights (access, deletion), a Data Processing Agreement with each processor, and control over any subprocessors. Think of EU hosting as necessary infrastructure groundwork that makes the rest far easier, not a complete solution on its own.

### What's the difference between data residency and data sovereignty?

Data residency is about physical location: where the data is stored. Data sovereignty is about legal jurisdiction: whose laws can compel access to it. A US-owned provider can store your data in an EU region (residency) while still being subject to US laws like the CLOUD Act that can require it to hand that data over (a gap in sovereignty). GDPR doesn't define "data sovereignty" as a legal term, but in practice teams treat it as infrastructure that's both located in and operated by an EU entity, so no non-EU jurisdiction has a direct claim to the data.

### Do I need a DPA if I use a hosting provider?

Yes, if that provider processes personal data on your behalf, which any host running your app or database does. GDPR Article 28 requires a contract (the Data Processing Agreement) governing that relationship. Without one, you as the data controller are not compliant, regardless of where the servers are. A DPA should come standard with your account; on Runsite a signed GDPR DPA is included on every plan with no negotiation or upcharge.

### Does a US company with an EU region count as EU hosting?

It satisfies data residency but not data sovereignty. The servers are in the EU, but a US-headquartered company remains subject to US jurisdiction and can be compelled to produce data under laws like the CLOUD Act. For many EU customers and public-sector buyers that distinction matters, and security questionnaires increasingly ask whether your infrastructure is operated by an EU entity rather than just located in the EU.

Keep reading

---

# S3 Egress Fees Explained: How to Calculate the Real Bill

> Storage is cheap; egress is where the object-storage bill comes from. How S3 data-transfer fees work, and how to calculate what you'll really pay.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. S3 Egress Fees: Why Downloading Your Own Data Costs So Much

StorageJuly 6, 20268 min read

# S3 Egress Fees: Why Downloading Your Own Data Costs So Much

Storage is the cheap line on an object-storage bill. Egress, the charge for data leaving the bucket, is where the money actually goes. Here's how S3 data-transfer fees work, how to calculate your real bill, and why zero-egress storage changes the math.

## The short version

Storage is the cheap, predictable line on an object-storage bill. Egress is the line that scales with how much the world uses your files, and it is where the money actually goes. Work out both numbers before the invoice does; if serving data is central to your product, zero-egress storage removes the one you cannot plan.

[Teo Marquardt](https://runsite.app/about#author)

You put a few hundred gigabytes of images on object storage, glance at the pricing page, and the numbers look friendly: storage is a couple of dollars a month, basically a rounding error. Then the first real invoice arrives and it's a hundred and something. Nothing about how much you're storing changed. The line that blew up isn't storage at all. It's egress, the charge for data leaving the bucket, and it's the part of the bill nobody shops on. Here's where that number comes from, how to work out your own before the invoice does it for you, and why some storage doesn't charge it at all.

## Why does the storage bill blow up after the first invoice?

The pricing page wasn't lying to you. Object storage genuinely is cheap to sit on. On [AWS S3](https://aws.amazon.com/s3/pricing/), standard storage runs about $0.023 per GB per month at the time of writing, so a hundred gigabytes of assets costs you around two dollars and change to keep. That's the number you compare when you're choosing where to put your files, and it's the number that makes object storage look like a solved problem.

The number you don't compare is egress: the fee for moving data out of the provider's network. It doesn't show up when you upload, and it doesn't show up while the files just sit there. It shows up every time someone downloads something. On most metered providers that runs somewhere around $0.09 per GB out to the internet once you're past a small free allowance (roughly the first 100 GB a month on AWS). Individually those are tiny fractions of a cent. The trouble is you're not billed once. You're billed per download, and a popular file gets downloaded a lot.

## What is egress, and where does it hide?

Egress is data transfer out: every byte that leaves the provider's network heading somewhere else. The reason it surprises people is that it isn't a single line you can point at on the pricing page. It's a multiplier that quietly attaches itself to things you'd never think of as "downloading." A few of the usual places it hides:

* **Serving assets to users.** Every image, video, PDF, or download your app hands to a visitor is egress. One 4 MB hero image on a page that gets a million views a month is four terabytes of transfer out, from a single file.
* **CDN origin pulls.** Putting a CDN in front of your bucket helps, but it isn't free either. Every cache miss means the CDN fetches the file from your bucket, and that origin pull is billed as egress. A cold cache, a low hit rate, or short cache lifetimes all quietly raise the number.
* **Cross-region and cross-AZ traffic.** Replicating a bucket to a second region for durability, or reading it from a service in a different availability zone, counts as data transfer too. It's egress you're paying to move data around inside the same provider.
* **Moving backups and exports out.** Pulling a database dump back down to restore it, shipping logs to an external tool, or handing a data export to a customer all leave the network, so all of them meter.
* **Retries and failed transfers.** A download that drops at 90% and restarts still billed you for that 90%. At scale, retries are a line item of their own.

None of these are exotic. They're the normal behavior of a normal app. That's the point: egress isn't a penalty for doing something wrong, it's a charge on the thing storage is for, which is handing files to people.

## How do you calculate your real S3 bill?

You can estimate your egress before a provider does it for you, and it's worth doing because the answer is often an order of magnitude off from what the storage line suggests. The rough shape of it is two separate sums that get added together:

text

```
storage cost = gigabytes stored        x  storage rate per GB
egress cost  = gigabytes served out    x  egress rate per GB

monthly bill = storage cost + egress cost
```

The storage side is easy: it's just how much you keep. The egress side is the one that runs away, because it isn't tied to how much you store, it's tied to how much you serve. Take a small media site that keeps 100 GB of images and video and serves 2 TB of it to visitors over a month. A busy blog with a bit of video is well into that range. Here's what the two lines look like side by side:

| Line      | Volume                                    | Rate      | Cost        |
| --------- | ----------------------------------------- | --------- | ----------- |
| Storage   | 100 GB                                    | $0.023/GB | $2.30       |
| Egress    | 1,900 GB (2 TB served, first 100 GB free) | $0.09/GB  | $171.00     |
| **Total** |                                           |           | **$173.30** |

Object storage bill for a media site over one month. Egress is 99% of the total, and it's the line you never shopped on.

The storage you actually shopped on is $2.30\. The bill is $173.30\. Everything in the gap is egress, and it scales with traffic, not with how much you store. Double your visitors next month and the storage line doesn't move while the egress line does. This is why the invoice feels disconnected from anything you decided: you sized your storage, but the meter runs on your audience. If you'd rather not run the arithmetic by hand, [plug your own storage and traffic into the pricing calculator](https://runsite.app/pricing/calculator) and watch the two lines move.

Check the rates before you trust the math

Provider egress prices change, vary by destination and region, and drop in tiers as volume climbs. The $0.09/GB and $0.023/GB above are round figures for AWS S3 to the public internet at the time of writing. Use them to understand the shape of the bill, then plug in your own provider's current numbers for a figure you can rely on.

## Why is expensive egress also vendor lock-in?

There's a second cost to egress that doesn't show up until you try to leave. Your data lives in the bucket, and the only way to move it somewhere else is to download it, which means egress. So the bigger your dataset and the higher the egress rate, the more it costs simply to walk out the door with the data you already own.

Say you've accumulated a terabyte and you want to migrate it to another provider. At around $0.09 per GB that's roughly $90 in transfer just to copy it out, before you've stored a single byte anywhere new, and that's a small dataset. For a serious archive the exit fee runs into real money. Egress isn't only an operating cost, then. It's an exit tax, and a provider that charges a lot for it has quietly made itself expensive to leave. Regulators have landed on the same conclusion: the [EU Data Act](https://digital-strategy.ec.europa.eu/en/policies/data-act) phases out cloud switching charges precisely because exit fees keep customers stuck. The cheaper it is to pull your data out, the freer you actually are to change your mind.

## What is zero-egress object storage?

Flip the pricing around and the whole picture calms down. When data transfer out is free, the egress line goes to zero and your bill collapses back to the one thing you can actually predict: the gigabytes you store. It isn't a fringe idea, either: Cloudflare R2, Backblaze B2, and Wasabi all drop or heavily discount egress, and Runsite takes the same stance. They don't all do it on the same terms, though, and [what each of them bills instead of transfer](https://runsite.app/blog/s3-alternatives-no-egress-fees) is the thing to read before you pick one. That's the model behind [zero-egress S3-compatible object storage](https://runsite.app/services/s3-storage): downloads, uploads, and transfer of any kind cost nothing, you pay only for what's on disk at €0.025 per GB a month after 5 GB free, and the data is hosted entirely in the EU under a GDPR DPA.

Run the same media site through it. The 100 GB of assets costs about €2.38 a month to store, and that's the whole bill, whether the site serves 2 TB or 20 TB to visitors. The line that was 99% of the invoice on a metered provider isn't discounted, it's simply gone. Your cost is flat, it scales with your library rather than your traffic, and a spike in popularity is good news instead of a billing event.

|                           | Metered S3 (typical) | Zero-egress (Runsite)    |
| ------------------------- | -------------------- | ------------------------ |
| Storage per GB-month      | \~$0.023             | €0.025 (first 5 GB free) |
| Egress per GB out         | \~$0.09              | €0.00                    |
| Bill to serve 2 TB        | \~$173               | \~€2.38                  |
| Exit fee to move 1 TB out | \~$90                | €0.00                    |
| Bill scales with          | Traffic              | Data stored              |
| S3 API compatible         | Yes                  | Yes                      |

Same 100 GB media site serving 2 TB a month. On a metered provider the traffic runs the meter; on zero-egress storage only the stored data is billed. Figures are each provider's own list price (AWS in USD, Runsite in EUR), not a currency conversion.

The migration worry disappears with it. Because the storage speaks the S3 API, the AWS SDK, boto3, or any S3 client works unchanged: you point the endpoint at the new host and swap the keys, and nothing in your code moves. What that compatibility actually guarantees, and the handful of features where it runs out, is the subject of [how S3-compatible object storage works](https://runsite.app/blog/s3-compatible-object-storage). And since pulling data out is free, there's no exit tax the day you decide to leave. Public buckets are served through a global CDN and private files through [time-limited presigned URLs](https://runsite.app/blog/s3-presigned-urls), so the CDN origin pulls and downloads that would have metered elsewhere just don't. If your workload is backups, the same logic makes an [off-site copy in your own bucket](https://runsite.app/blog/postgresql-backup-to-s3) cheap to write and, more importantly, free to restore when you actually need it.

## When does egress actually matter, and when doesn't it?

It's worth being honest about this, because egress isn't a crisis for everyone. If you're serving a lot of bytes to a lot of people, it's probably the biggest number on your bill and the one most worth designing around:

* **Media and video.** Images, audio, and especially video are large and served repeatedly, which is exactly the shape that runs the egress meter hot.
* **Public assets at scale.** Anything popular and downloadable: a busy site's static files, a widely used app's downloads, user-generated content with real traffic.
* **Frequent restores or data movement.** Backups you pull back often, datasets you shuttle between environments, exports you hand to customers.

And if that's not you, don't let the horror stories push you into over-engineering. A low-traffic internal tool, a private bucket that's written far more than it's read, or an archive that mostly sits untouched will barely register any egress at all. On that kind of workload the difference between metered and free transfer is a few cents, and it isn't worth losing sleep over. The reason to care is the shape of your traffic, not the fact that egress exists.

The short version: storage is the cheap, predictable part, and egress is the part that scales with how much the world uses your files. Work out your own two lines before the invoice does, and if serving data is central to what you're building, [store and serve without the bandwidth bill](https://runsite.app/services/s3-storage) so the only number you're paying for is the one you can plan around.

Related service

## S3 Object Storage on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore S3 Object Storage](https://runsite.app/services/s3-storage)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Why is S3 egress so expensive?

Egress is priced per gigabyte of data leaving the provider's network, and unlike storage, which you pay for once, you pay it every single time a file is downloaded. A popular asset served thousands of times a day meters on every request, so the total climbs with your traffic rather than your storage. At roughly $0.09 per GB on AWS at the time of writing, a site serving a couple of terabytes a month can see egress dwarf its storage cost many times over.

### How do I calculate my S3 data-transfer cost?

Estimate the gigabytes you serve out in a month, subtract any free allowance, and multiply by the provider's egress rate per GB. That's a separate number from your storage cost, and usually a much larger one, since storage is just gigabytes stored times the storage rate. For example, serving 2 TB with 100 GB free at $0.09/GB is 1,900 GB × $0.09 = $171, against maybe $2.30 to store 100 GB. Always confirm the current rate with your provider, since egress pricing changes and varies by region and destination.

### Is there object storage with no egress fees?

Yes. Several providers, including Cloudflare R2, Backblaze B2, Wasabi, and Runsite, don't charge for data transfer at all, so downloads, uploads, and transfer of any kind are free and you pay only for the gigabytes you store. Runsite's object storage works this way and is hosted in the EU: it's S3-compatible, so your existing AWS SDK or boto3 code runs unchanged, and it bills a flat €0.025 per GB per month after 5 GB free with zero egress charges. That makes the bill scale with your stored data rather than your traffic, and it removes the exit fee for moving data back out.

### Is there an S3 egress calculator?

You can estimate egress in one line: gigabytes served out in a month, minus any free allowance, times the provider's rate per GB, then add that to your storage cost. To skip the arithmetic, run your storage and expected traffic through Runsite's pricing calculator at runsite.app/pricing/calculator, which shows a metered bill and a flat zero-egress bill side by side so you can see how much of the total is transfer.

Keep reading

---

# Data Processing Agreement for SaaS: What to Sign

> Whether your SaaS signs a DPA as controller or processor, the clauses GDPR Article 28 requires inside it, and how to stop it blocking a deal.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Data Processing Agreement for SaaS: What Article 28 Requires

ComplianceJuly 8, 202610 min read

# Data Processing Agreement for SaaS: What Article 28 Requires

A deal stalls because their legal team wants your DPA before signing. Here's what GDPR Article 28 requires inside a Data Processing Agreement, which side of it your SaaS is on, and how to keep it from becoming a bottleneck in your sales cycle.

## The short version

A DPA is the contract that makes it lawful to process personal data on another company's behalf, and GDPR Article 28 makes it mandatory whenever a controller uses a processor. Most SaaS is both at once, so you need one in each direction. Watch the subprocessor chain — that is where residency and accountability slip.

[Teo Marquardt](https://runsite.app/about#author)· Updated August 21, 2026

At some point a deal you thought was done stops moving. The product fit was there, the pricing was agreed, and then their legal or security team sends back one line: "We'll need your DPA before we can sign." If you've never dealt with one, that sentence can feel like a wall. It isn't. A Data Processing Agreement is a standard, well-defined contract, and for most SaaS products it's a document you should already have ready to hand over rather than something you scramble to produce mid-deal.

This is the practical version, written for the founder or engineer who has to understand the thing rather than draft it from scratch. It covers what a DPA actually is, why GDPR requires one, what has to be inside it, the two directions you'll deal with it from (signing one and requesting one), and how to stop it from becoming a bottleneck in your sales cycle. None of it is legal advice, but all of it is the context that makes the legal advice quick and cheap.

## What a data processing agreement is, and when a SaaS needs one

A Data Processing Agreement is a contract between two parties: the one that decides what happens to personal data, and the one that handles that data on the first party's behalf. In GDPR's language those are the data controller and the data processor. The DPA is the document that pins down, in writing, exactly what the processor is allowed to do with the data, what it must protect, and what happens when the relationship ends. You'll also see it called a Data Processing Addendum — AWS, Google, and Microsoft all use that name — but it's the same instrument.

That's the whole idea. It isn't a privacy policy (that's the promise you make to your users), and it isn't your terms of service (that's the commercial relationship). A DPA sits underneath both and governs one specific thing: how personal data moves between two companies when one is processing it for the other.

The reason it exists at all is accountability. GDPR makes the controller responsible for personal data across its entire lifecycle, including every company it hands that data to. Since a controller can't personally supervise how its vendors run their servers, the DPA is the mechanism that carries the obligations down the chain in writing. It turns "we trust our host to be careful" into "our host is contractually bound to specific, enforceable requirements."

## Why GDPR Article 28 makes it mandatory, not optional

The requirement isn't a best practice someone recommends. It's written into the regulation. [Article 28 of the GDPR](https://eur-lex.europa.eu/eli/reg/2016/679/art%5F28) states that whenever a controller uses a processor, the processing must be governed by a contract that sets out the subject matter, duration, nature and purpose of the processing, the type of personal data, the categories of people it belongs to, and the obligations of both sides.

Read that plainly: if a company processes personal data on your behalf and there's no DPA, you (the controller) are not compliant. The gap isn't the processor's problem to fix on their own — the accountability lands on you. This is why the customer's legal team asks for yours before they sign. By buying your SaaS and feeding it their users' data, they're making you their processor, and their own compliance depends on having that contract in place with you.

The same logic runs in the other direction. Every third-party service you use to run your product — your hosting provider, your database, your email sender, your analytics — is your processor, and you need a DPA with each of them. Article 28 doesn't care about company size or deal value. A two-person startup handling EU personal data has the same baseline obligation as an enterprise. How much of your stack has a processor attached to it in the first place depends on the service model you bought: [the responsibility line between IaaS and PaaS](https://runsite.app/blog/paas-vs-iaas-responsibility) sets out which layers come with a contract and which ones you are carrying alone.

The exception is narrow: if a service never touches personal data, or only handles data you've fully anonymised so no individual can be re-identified, there's no processing of personal data and no DPA to sign. That's a smaller set than people hope. Pseudonymised data — hashed emails, user IDs — still counts as personal data under GDPR, so it doesn't get you off the hook.

What happens if you skip it? Missing DPAs are a documentation failure regulators look for first, because they're trivial to check, and non-compliance with Article 28 falls under the higher tier of [GDPR fines](https://eur-lex.europa.eu/eli/reg/2016/679/art%5F83) — up to €20 million or 4% of global annual turnover. For most small teams the fine is the distant risk; the immediate one is the deal that stalls when a buyer's security review asks for a document you don't have.

## Controller or processor? For a SaaS, usually both

The single most common source of confusion is thinking you're only ever one of these. In practice a typical SaaS company wears both hats at once, and which one applies depends on whose data you're looking at.

For your customers' data — the records their users create inside your app — your customer is the controller and you are the processor. They decide why the data exists and what happens to it; you just operate the software that stores and processes it. In this relationship, you provide the DPA and they sign it.

For your own operational data — your customers' account details, your billing records, your marketing contacts — you are the controller. And every vendor you use to handle that data is your processor. In this relationship, you request and sign their DPAs.

| Whose data                                               | Your role  | Who provides the DPA            |
| -------------------------------------------------------- | ---------- | ------------------------------- |
| Your customers' users — records created inside your app  | Processor  | You provide it to your customer |
| Your own operational data — accounts, billing, marketing | Controller | Each vendor provides it to you  |

The same SaaS is a processor for its customers' data and a controller for its own.

So a healthy SaaS ends up with two stacks of paper: the DPA you hand to customers as their processor, and the DPAs you've collected from your own vendors as a controller. Getting the roles straight is what makes the rest of this manageable, because the duties, and who drafts the document, flip depending on which side you're on.

## What Article 28 requires inside a data processing agreement

You don't need to draft one from memory, but you should recognise the parts, because a DPA that's missing them isn't doing its job. Article 28(3) lays out the required content, and a solid agreement covers each of these:

* **Scope and purpose** — what personal data is processed, what categories of people it belongs to, and why. The processor may only act on the controller's documented instructions.
* **Duration** — how long the processing lasts, and what happens to the data at the end: return it or delete it, at the controller's choice.
* **Confidentiality** — everyone who touches the data is bound to keep it confidential.
* **Security measures** — the technical and organisational safeguards protecting the data, such as encryption, access control, and backups.
* **Subprocessors** — the rules for the processor using its own vendors, including your right to know who they are and to object.
* **Assistance and audits** — the processor must help the controller answer user-rights requests and demonstrate compliance, and allow audits.
* **Breach notification** — the processor must tell the controller about a data breach without undue delay so the controller can meet its own reporting deadlines.

Most reputable providers publish a standard DPA that already covers all of this. Your job is usually to read it, confirm it matches how you actually operate, and sign — not to write one clause by clause.

## The subprocessor chain

Subprocessors deserve their own moment, because they're where a clean-looking compliance story springs a leak. A subprocessor is a processor's own processor: a vendor your vendor relies on. When you use a hosting platform that in turn runs on someone else's underlying cloud, that underlying cloud is your subprocessor, one step removed.

This matters for two reasons. First, the chain doesn't dilute your accountability. As the controller, you remain responsible for personal data no matter how many layers down it travels, so a subprocessor in a jurisdiction you didn't expect is still your problem. Second, it's a common way EU data ends up leaving the EU: your provider is European, but one of its subprocessors runs in the US, and now your data is subject to a transfer you never signed off on. And disclosure alone doesn't make that transfer lawful. Once personal data leaves the EU, GDPR's Chapter V kicks in and the transfer needs its own legal basis — an adequacy decision or Standard Contractual Clauses. The DPA's subprocessor clause tells you the transfer is happening; it's the transfer mechanism that makes it legal. Two separate boxes to tick, and it's easy to assume the first one covers the second.

A good DPA gives you visibility and a say here: a published list of subprocessors, advance notice before a new one is added, and the right to object. When you're evaluating a provider, the subprocessor list is one of the more revealing documents you can read — it tells you where your data really goes, not just where the marketing page says it's hosted. This is also why an EU-native stack, where the provider and its subprocessors are all inside the EU, is simpler to reason about than a global platform with an EU region bolted on.

## Why the DPA shouldn't cost you a sales call

Here's the part that trips up smaller teams. On plenty of developer platforms the DPA technically exists, but it's gated: available only on an enterprise plan, or behind a "contact sales" form, or as a manual request that takes a week to fulfil. For a solo founder or a small team, that turns a routine legal necessity into a procurement obstacle. Worse, plenty of people ship without one simply because getting it was too much friction, which leaves them non-compliant without realising it.

That's the wrong default, and it's worth naming as a signal when you choose infrastructure. A DPA costs a provider essentially nothing to include — it's a standard document — and every customer processing EU personal data needs one. Gating it behind a sales tier doesn't reflect a real cost; it reflects a choice to treat compliance as an upsell. The better model is simple: the DPA is part of the account from day one, downloadable and signed without a conversation.

That's how [Runsite](https://runsite.app/services/web-services) handles it. A signed GDPR Data Processing Agreement is included on every plan — no upgrade, no negotiation, no sales call — and the whole stack runs in Germany, so the subprocessor question has a short answer too. When a customer's security review asks for your DPA, you're forwarding a document you already have, not opening a two-week detour. If you want the wider picture of what that involves, our guide to [EU hosting, data residency, and GDPR for developers](https://runsite.app/blog/eu-hosting-for-developers) covers the surrounding ground.

## A DPA checklist for your SaaS, both directions

The whole topic gets lighter once you treat it as two short checklists rather than a legal ordeal.

As a processor (handing your DPA to customers): have a standard DPA ready before anyone asks. Publish it or keep it one click away, make sure it reflects how you actually process data, and keep your own subprocessor list current so you can answer follow-up questions without a fire drill.

As a controller (collecting DPAs from your vendors): when you add a service that will touch personal data, get its DPA signed as part of onboarding, not months later. Keep the signed copies somewhere you can find them during an audit or a customer review, and glance at each vendor's subprocessor list so you know where the data actually flows.

Do that and the request that once stalled a deal becomes a two-minute reply. The compliance was already in place; you just needed the paperwork to prove it.

## The short version

A Data Processing Agreement is the contract that makes it legal for one company to process personal data on another's behalf. GDPR's Article 28 makes it mandatory whenever a controller uses a processor, which means your customers need one from you, and you need one from every vendor that touches personal data. A typical SaaS is a controller and a processor at the same time, so you'll deal with DPAs from both directions. Watch the subprocessor chain, because that's where accountability and data residency tend to slip. And treat a provider that hides its DPA behind a sales call as a warning sign: the document is standard, every customer needs it, and it should come with the account by default. [Build on infrastructure that includes a GDPR DPA on every plan](https://runsite.app/services/web-services) and the next time legal asks, the answer is already signed.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does GDPR Article 28 require?

Article 28 requires that any processing carried out on a controller's behalf be governed by a written contract, and it specifies what that contract must set out: the subject matter and duration of the processing, its nature and purpose, the type of personal data and the categories of people it belongs to, and the obligations of both parties. It also requires the processor to act only on documented instructions, keep the data confidential, apply appropriate security measures, get authorisation before engaging subprocessors, assist with data subject requests, and delete or return the data at the end of the relationship. A DPA is the document that carries those terms.

### What is a Data Processing Agreement (DPA)?

A DPA is a contract between a data controller and a data processor that governs how personal data is handled. It sets out what data is processed, for what purpose and how long, the security measures around it, the rules for subprocessors, and what happens to the data when the relationship ends. Under GDPR's Article 28 it's mandatory whenever one company processes personal data on another's behalf, which includes any SaaS handling its customers' data and any hosting provider running that SaaS.

### When is a data processing agreement required?

Whenever one company processes personal data on another company's behalf. That's the trigger in GDPR's Article 28 — not a revenue threshold, not a headcount, and not whether the data feels sensitive. If you store, host, analyse, or otherwise handle personal data that someone else decides the purpose for, the arrangement needs a written agreement before the processing starts. In practice that means one agreement flowing to each of your customers, and one flowing back from every vendor in your stack that touches personal data: hosting, database, email, analytics, error tracking.

### Do I need a DPA for my SaaS?

Yes. GDPR's Article 28 has no exemption for company size or revenue. If you process personal data on your customers' behalf, they need a DPA from you, and their own compliance depends on it — which is why enterprise buyers ask for it during security review. Equally, you need a signed DPA from every vendor that processes personal data for you, such as your hosting provider, database, and email service. The obligation applies to a two-person startup exactly as it does to a large company.

### Am I a data controller or a data processor?

For most SaaS companies, both — it depends on whose data you're looking at. For the data your customers' users create inside your app, your customer is the controller and you're the processor, so you provide the DPA. For your own data (customer accounts, billing, marketing), you're the controller and each vendor you use is your processor, so you request their DPA. Getting this split straight tells you which direction each DPA flows.

### What's a subprocessor, and why does it matter?

A subprocessor is your processor's own vendor — for example, the underlying cloud your hosting provider runs on. It matters because your accountability as a controller follows the data all the way down the chain, and because a subprocessor in another jurisdiction is a common way EU data ends up outside the EU. A good DPA gives you a published subprocessor list, advance notice of changes, and the right to object.

### Should a DPA cost extra or require a sales call?

No. A DPA is a standard document that costs a provider nothing to include, and every customer processing EU personal data needs one, so gating it behind an enterprise plan or a "contact sales" form treats a legal necessity as an upsell. The better default is a signed DPA available on every plan without negotiation. On Runsite a GDPR DPA is included on every plan, so it's ready the moment a security review asks for it.

Keep reading

---

# GDPR Data Residency: Where to Store EU User Data

> GDPR has no hard rule that EU personal data must stay in Europe, but transfers out need a legal mechanism. Where EU user data must live, and how to decide.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Where to Store EU User Data: What GDPR Actually Requires

ComplianceJuly 10, 20269 min read

# Where to Store EU User Data: What GDPR Actually Requires

A customer's security review asks where their data physically lives and whether it leaves the EU. GDPR has no hard rule that personal data must stay in Europe — but every transfer out needs a legal mechanism, and EU residency is the simplest way to skip that problem entirely.

## The short version

GDPR does not require personal data to stay in the EU. It regulates transfers out of it, and every transfer needs a legal mechanism whose survival you do not control. Keeping every copy inside the EU — backups, replicas, logs, CDN and subprocessors included — skips that problem rather than managing it.

[Teo Marquardt](https://runsite.app/about#author)

A security questionnaire arrives from a customer you want to close, and two of the questions stop you: "Where is our data physically stored?" and "Is any personal data transferred outside the EU?" If you have to go and find out, the honest answer is usually more complicated than you'd like. Your database is in one place, its backups somewhere else, your file storage on a third provider, and your error tracker on a fourth. Suddenly a deal is waiting on a map of where every copy of the data actually lives.

This is the practical version of that map, written for the founder or engineer who has to answer the question rather than litigate it. It covers what GDPR really says about storage location, what "data residency" means once you look past the primary database, why storing EU data in the US got legally awkward, and a way to decide where your data should live without a legal department. None of it is legal advice, but all of it is the context that makes the legal advice fast.

## Does GDPR actually require you to keep data in the EU?

The short answer surprises people: no. GDPR contains no blanket rule that personal data must physically stay inside the European Union. There is no line that says "store it in Europe." What the regulation controls is something narrower: the transfer of personal data out of the EU to a country that isn't covered by EU data-protection law.

That's the subject of [Chapter V of the GDPR](https://eur-lex.europa.eu/eli/reg/2016/679/art%5F44) (Articles 44 to 49). The rule it sets is conditional: you may transfer personal data to a country outside the EU, but only if there's a legal mechanism behind it. That mechanism is one of three things: an adequacy decision from the European Commission that says the destination country protects data to an EU-equivalent standard (Article 45), appropriate safeguards such as Standard Contractual Clauses signed between the parties (Article 46), or one of the narrow situational exceptions in Article 49.

So the accurate framing is this: you can store EU personal data outside the EU, but the moment you do, you take on the transfer-mechanism problem: you have to have one and be able to show it's still valid. Keeping the data inside the EU is simply the way to make that entire problem disappear. There's no third country, no adequacy question, no clauses to maintain. That's why "data residency" and "GDPR compliance" get fused together in people's heads: residency isn't a legal requirement, it's the shortcut past the hardest part of the law.

The distinction that matters

GDPR doesn't mandate EU storage — it regulates transfers out of the EU. Storing data in Europe isn't the rule; it's the option that means you never have to satisfy the rule about transfers in the first place.

## What data residency actually means

Once you accept that residency is worth having, the next trap is thinking it's about where your primary database happens to run. It isn't. Data residency is a property of every copy of the data, and a running application produces a surprising number of copies.

* The primary database, plus any read replicas you've spun up for scale.
* Automated backups, WAL archives, and point-in-time-recovery snapshots, which are full copies of your data and easy to forget.
* Object storage for user uploads and media, plus any cross-region replication it does for durability.
* Caches and session stores — a Redis instance holding session tokens is holding personal data.
* Logs and their aggregation, which routinely capture request bodies, IP addresses, and user identifiers.
* Your CDN's origin and edge caches, and where it keeps its access logs.
* Every subprocessor your providers use underneath — the cloud your host runs on, the region your email vendor sends from.

The failure mode is almost never the obvious one. It's a provider whose control panel shows "Frankfurt" while its backups replicate to a US region for safety, or a CDN that serves European users from the edge but pulls from a US origin and writes its logs there. Residency is the answer to "where does every copy of this data end up," not "which region did I pick at signup."

## Why "just store it in the US" got complicated

For a long time the pragmatic answer was to run on a big US cloud and rely on whatever transfer framework was in fashion. That path got rougher in July 2020, when the Court of Justice of the EU issued its ruling in the case commonly known as [Schrems II](https://runsite.app/blog/schrems-ii-data-transfers). The court struck down the EU-US Privacy Shield, the adequacy framework that thousands of companies had relied on to move data across the Atlantic. Overnight, that easy legal basis was gone.

Standard Contractual Clauses survived the ruling, but with a condition attached: you can't just sign them and move on. You're expected to assess whether the destination country's laws actually let you honour them (a transfer impact assessment) and add supplementary measures where they don't. That's real work, and the burden sits with you, the data exporter.

In July 2023 the Commission adopted a new [adequacy decision for the EU-US Data Privacy Framework](https://commission.europa.eu/law/law-topic/data-protection/international-dimension-data-protection/eu-us-data-privacy-framework%5Fen), which restored a cleaner legal path, but only for US organisations that self-certify under it, and against a backdrop where the two previous US frameworks were both eventually invalidated in court. A challenge that people already call "Schrems III" is widely expected. Storing EU data in the US is legal today, but it rests on a framework with a track record of being struck down and re-negotiated.

Keeping the data in the EU doesn't depend on any of that machinery. There's no adequacy decision to watch, no clauses to keep current, no assessment to redo when the legal ground shifts. The trade is straightforward: US storage is legal but fragile and high-maintenance; EU storage is legal and stable. For most teams that don't have a specific reason to be in the US, the second one is a lot less to think about.

## Where EU data leaks out without you noticing

If you decide you want EU residency, the work isn't picking a region. It's finding the copies that quietly leave it. These are the usual places EU data slips across a border while the main database sits obediently in Frankfurt:

* A managed database that keeps read replicas or automated backups in a non-EU region — the live data is in the EU, the copies aren't.
* Object storage configured to replicate buckets to a US region for durability, so every uploaded file exists on both sides of the Atlantic.
* A CDN that caches at European edges but fetches from a US origin, or writes its access logs, full of IPs and request paths, to US infrastructure.
* Analytics, error tracking, and session-replay tools that ship event payloads and stack traces to US servers by default.
* Transactional email and SMS providers that route messages, and keep delivery logs, through non-EU infrastructure.
* Support desks and CRMs where a copy of your users' personal data lands in a US tenant the moment a ticket is opened.

Each of these is a transfer under Chapter V, and each needs its own legal basis. This is exactly the gap Schrems II is about: the data doesn't leave through the front door, it leaves through a subprocessor two layers down. It's also why a stack assembled from a dozen mixed-region services is genuinely hard to answer for: the honest response to "does any personal data leave the EU" requires you to know the footprint of every vendor, not just your own database.

## A practical way to decide where to store EU user data

You don't need a policy document for this. You need to answer a few questions in order and let the answers point at the setup.

1. Map the data. Write down what personal data you hold and where each copy lives — database, backups, file storage, logs, and every third-party service that touches it. Most of the risk is in copies you forgot you were making.
2. Decide whether you actually need strict residency. EU enterprise customers and their security reviews will usually want a firm "it stays in the EU." A side project with no EU B2B pipeline may reasonably care less. Be honest about which you are rather than over- or under-engineering it.
3. If you're keeping it in the EU, choose providers that run and back up inside the EU with no non-EU replication, and read their subprocessor list before you commit — that's where the leaks hide.
4. If you genuinely need a US service, treat it as a transfer: get that specific vendor covered by a mechanism (DPF certification or SCCs plus a transfer impact assessment), and accept that you've taken on its legal fragility.
5. Get the DPA either way. Residency answers "where" the data lives; the [Data Processing Agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) answers "under what terms" it's handled. You need both.

One honest caveat worth stating plainly: residency is not the same as compliance. Keeping every byte in Frankfurt doesn't give you a lawful basis for processing, a way to handle data-subject requests, or adequate security on its own. What it does is remove the single hardest and least stable question (international transfers), so the rest of your compliance work isn't sitting on shifting legal ground.

## The simplest answer: an EU-only stack

The reason residency turns into an audit is fragmentation — a database here, backups there, files on a third service, each with its own region settings and its own subprocessors. The unglamorous fix is to keep the whole stack in one EU region, so there's simply nothing to transfer and nothing to reconcile.

That's how [Runsite](https://runsite.app/blog/eu-hosting-for-developers) is set up. [Managed PostgreSQL](https://runsite.app/services/postgresql) keeps the instance, its WAL archive, and every automated backup and point-in-time-recovery snapshot in Germany, so a restore never pulls data across a non-EU boundary. [S3-compatible object storage](https://runsite.app/services/s3-storage) holds your files in the same region, encrypted at rest with AES-256 and never replicated to a non-EU region. Public assets are served through a CDN, private ones through time-limited presigned URLs. A signed GDPR Data Processing Agreement comes with every plan, so the "under what terms" question is answered too.

When the database, its backups, and your file storage all sit in the same EU region, that security questionnaire stops being a research project. "Where is our data physically stored?" has one answer, and "does any personal data leave the EU?" is a plain no. Not because you audited a sprawl of vendors, but because there was never a copy anywhere else to begin with.

## The short version

GDPR doesn't force you to store personal data in the EU; it regulates transfers out of it, and every such transfer needs a legal mechanism whose stability you don't control. Data residency, keeping every copy inside the EU, is the shortcut past that whole problem, and it's a property of your entire footprint (backups, replicas, logs, CDN, and subprocessors), not just the primary database. Work out whether your customers need strict residency, map where each copy actually lives, and if the answer has to be "in the EU," the least fragile way to get there is a stack that never leaves it. [Run your database and storage in one EU region](https://runsite.app/services/postgresql), and the next time a security review asks where the data lives, the answer is already one line.

Related service

## Managed PostgreSQL on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed PostgreSQL](https://runsite.app/services/postgresql)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Does GDPR require personal data to be stored in the EU?

No. GDPR has no blanket rule that personal data must physically stay in the European Union. What it regulates, in Chapter V (Articles 44 to 49), is the transfer of personal data to countries outside the EU — those transfers are only allowed with a legal mechanism such as an adequacy decision, Standard Contractual Clauses, or a specific derogation. Keeping data in the EU isn't a legal requirement in itself; it's the simplest way to avoid the transfer-mechanism obligation entirely.

### What is data residency under GDPR?

Data residency is where your data physically lives — but under GDPR the meaningful version is where every copy lives, not just the primary database. That includes read replicas, automated backups and point-in-time snapshots, object storage, caches and session stores, logs, your CDN's origin and access logs, and the subprocessors your providers use underneath. A setup is only truly EU-resident if none of those copies leave the EU.

### Can I store EU user data in the US?

Yes, but it's a transfer under GDPR's Chapter V and needs a legal basis. Since the Schrems II ruling in 2020 invalidated Privacy Shield, that means either the recipient is certified under the EU-US Data Privacy Framework (the 2023 adequacy decision), or you rely on Standard Contractual Clauses backed by a transfer impact assessment. It's legal today, but the framework rests on a history of US arrangements being struck down, so it carries maintenance and legal-uncertainty costs that EU storage doesn't.

### Is data residency the same as GDPR compliance?

No. Keeping data in the EU removes the hardest and least stable compliance question (international transfers), but it doesn't give you a lawful basis for processing, a process for handling data-subject requests, adequate security, or a Data Processing Agreement. Residency is one important piece of compliance, not the whole of it. This isn't legal advice; treat it as the context that makes a conversation with a lawyer shorter.

Keep reading

---

# EU vs US Hosting: Latency, Law, and Cost Compared

> EU or US hosting isn't about which region is better. It's three trade-offs: latency, legal exposure, and cost. How each axis breaks down, and how to pick.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. EU vs US Hosting: Latency, Law, and Cost

ComplianceJuly 13, 202610 min read

# EU vs US Hosting: Latency, Law, and Cost

Choosing between EU and US hosting isn't a contest for the "better" region. It's three separate trade-offs — latency, legal exposure, and cost — and each one points at a different thing. Here's how each axis actually breaks down, and a simple way to pick.

## The short version

This is three decisions, not one. Latency is physics: put the origin near your users, and note that a CDN speeds static assets rather than dynamic requests. Law is about stability: US hosting of EU data rests on transfer frameworks with a record of collapsing. Cost is about borders, not per-gigabyte rates.

[Teo Marquardt](https://runsite.app/about#author)

At some point in setting up a new project you hit a dropdown labelled "region," and the choice feels bigger than a dropdown should. Europe or the United States? It looks like a coin flip, and plenty of teams treat it as one, then spend the next year explaining a slow app to European users or a surprise question in a security review. The decision isn't hard because the options are close. It's hard because "EU or US" is really three different questions wearing one label.

This walks through those three questions in order: how fast the app feels, what legal weight the location carries, and where the money actually goes. Then it ends with a way to pick that doesn't rely on a gut feeling. None of it is legal advice. It's the engineering and business context that makes the region dropdown a decision instead of a guess.

## The wrong question: which region is "better"?

There is no better region in the abstract. A US region isn't faster or cheaper or safer than an EU one as a general fact; it depends entirely on what you're measuring against. The reason the choice feels slippery is that three independent things get bundled into it, and they don't all point the same way. Once you separate them, each one has a clear answer.

| Axis    | What it actually measures              | Points you toward                           |
| ------- | -------------------------------------- | ------------------------------------------- |
| Latency | How far requests physically travel     | The region closest to most of your users    |
| Law     | Which country's rules and courts apply | The jurisdiction where your obligations sit |
| Cost    | Where paid borders sit in your stack   | The region with the fewest of them          |

EU vs US hosting is three separate trade-offs, not one verdict.

Read that as three compasses rather than one scoreboard. Each axis answers a "closest to what" question, and the useful work is figuring out what each one is closest to for your specific app. Sometimes all three needles land on the same region and the decision is easy. When they don't, you at least know which trade-off you're making instead of averaging them into a shrug.

## Latency: the axis that's pure physics

Latency is the one axis nobody gets to argue with, because it's set by the speed of light. Light travels through fibre at roughly two-thirds of its vacuum speed, which works out to about one millisecond of one-way delay for every 200 kilometres of cable. That's a floor, not an estimate: no amount of engineering moves data faster than that, and real routes are never straight, so the practical number is always worse.

Frankfurt to the US East Coast is roughly 6,000 kilometres of ocean. A round trip across it costs somewhere in the region of 80 to 90 milliseconds before your server has done a single thing: no query, no render, just the packet getting there and an acknowledgement coming back. Cross to the US West Coast and it's worse. For a European user hitting a US-hosted app, that tax is added to every request that has to reach the origin, and it stacks: a page that makes a handful of sequential calls can feel sluggish purely from distance.

What a CDN does and doesn't fix

A CDN caches static assets — images, scripts, stylesheets — at edge locations near your users, and for those it erases the distance. What it can't cache is the dynamic part: a login, a checkout, anything that writes to the database or is unique per user. Those still travel all the way to the origin and back, so a CDN in front of a distant server speeds up the wrapper while the app underneath stays slow.

So the latency rule is simple and it's not EU-flavoured: put the origin near the majority of your users. If your audience is mostly European, an EU region wins the requests a CDN can't touch. If it's mostly American, a US region wins for exactly the same reason, and hosting in Europe would be the mistake. Latency doesn't care about your compliance story; it cares about a map.

## Law: convenience now versus stability later

The legal axis is where "EU or US" stops being about performance. If you hold personal data on people in the EU, storing it in the US is a transfer of that data outside the Union, and GDPR treats transfers as something you have to justify rather than something you're free to do. We covered the mechanics in [where to store EU user data](https://runsite.app/blog/gdpr-data-residency); the short version is that a transfer needs a legal basis, and the legal bases for the US have a habit of collapsing.

The 2020 [Schrems II ruling](https://runsite.app/blog/schrems-ii-data-transfers) struck down the Privacy Shield framework thousands of companies relied on. A newer adequacy decision, the [EU-US Data Privacy Framework](https://commission.europa.eu/law/law-topic/data-protection/international-dimension-data-protection/eu-us-data-privacy-framework%5Fen), restored a cleaner path in 2023, but it applies only to US organisations that self-certify under it and sits against a history of such arrangements being invalidated in court. On top of that, the [US CLOUD Act](https://www.congress.gov/bill/115th-congress/house-bill/4943) lets US authorities compel US-based providers to hand over data they control regardless of which region it's stored in. None of this makes US hosting illegal. It makes it legal-but-conditional: it works until the framework underneath it is renegotiated again.

Keeping EU data in the EU sidesteps the whole apparatus. There's no transfer to justify, no framework to monitor, no assessment to redo when the legal ground shifts. For a team selling into European enterprises, that's not an abstraction; it's the difference between a security questionnaire that takes a paragraph and one that takes a lawyer. The broader picture, including the GDPR roles and the DPA that goes with them, is in the [EU hosting guide](https://runsite.app/blog/eu-hosting-for-developers). The point for this decision: on the legal axis, US hosting buys convenience today against fragility later, and EU hosting buys stability.

## Cost: it's not per-GB, it's per-border

Cost is the axis people expect to be a simple price comparison, and it usually isn't. Raw compute doesn't differ enough between a reputable EU host and a US hyperscaler like AWS to drive the decision. What actually moves the bill is borders — the points where data crosses between regions or leaves for the public internet, each of which tends to carry a charge. The relevant question isn't "which region is cheaper per gigabyte," it's "how many paid borders does this architecture create."

* Egress and cross-region transfer. Moving data out to the internet, or replicating it between a US region and an EU one for latency or durability, is often the largest line on an object-storage or bandwidth bill. We broke the maths down in [S3 egress fees](https://runsite.app/blog/s3-egress-fees); a single-region stack simply never generates the cross-region version of that charge.
* Currency and tax friction. A European business billed in dollars carries exchange-rate exposure and the overhead of reconciling foreign invoices and VAT, none of which shows up in the sticker price but all of which is real cost.
* The hidden compliance bill. The moment EU data lands in the US, you inherit the work of keeping the transfer lawful: transfer impact assessments, Standard Contractual Clauses to maintain, subprocessor lists to audit. That's engineering and legal time, and it recurs every time the legal ground moves.

Put plainly: US compute isn't inherently pricier than EU compute. Borders are what cost money, and a stack split across two continents has more of them — in bandwidth, in currency, and in compliance overhead — than one that stays in a single region.

## A framework: pick the region closest to what matters

You don't need to weigh the three axes against each other with a spreadsheet. You need to answer three questions honestly and see where they land.

1. Where are most of your users? That answers latency. Host the origin near the bulk of your traffic; a CDN handles the static assets for everyone else. If your audience straddles both continents, weight it by where the requests a CDN can't cache come from.
2. Whose rules do you actually have to satisfy? That answers law. If you hold EU personal data and sell to EU customers, EU hosting removes the transfer question entirely. If your obligations and your users are American, a US region is the honest answer and EU hosting would only add friction.
3. How many paid borders does the choice create? That answers cost. A single-region stack avoids cross-region egress, currency friction, and the recurring compliance overhead of a transfer. Count the borders before you count the per-gigabyte price.

When the three answers disagree, they at least disagree legibly. A US-facing product with a few European users belongs in the US, with a CDN and, if it touches EU data, a proper transfer mechanism. There's no universal winner here, and it's worth saying clearly that hosting in the EU is not a compliance certificate on its own. Residency removes the hardest question, but a lawful basis, data-subject processes, and security are still yours to handle.

## When all three axes point to the EU

For a large share of teams — anyone building for a mostly European audience while holding data on European users — the three needles land on the same spot. The users are in Europe, so latency wants an EU origin. The obligations are European, so the law wants EU residency. And keeping everything in one region is what keeps the border count, and the bill, down. At that point the interesting question isn't the region, it's how to keep the whole stack there without it fragmenting across a dozen providers. If you're arriving at that question while planning a move off a US platform, the practical version of it is worked through in [choosing a European Heroku alternative](https://runsite.app/blog/european-heroku-alternative).

That's the setup Runsite is built around. [Web Services](https://runsite.app/services/web-services) run your app containers, environment variables, build artifacts, and logs in a German region, with no replication to non-EU regions and no fallback to overseas infrastructure. Put [managed PostgreSQL](https://runsite.app/services/postgresql) and [S3-compatible object storage](https://runsite.app/services/s3-storage) alongside them and the database, its backups, and your files sit in the same region rather than scattered across borders. A signed GDPR Data Processing Agreement comes with every plan, so the legal axis is answered in the contract rather than as an enterprise upsell.

The payoff is that the three questions collapse into one region. European users get an origin on their continent for the requests a CDN can't cache, the transfer question never arises because nothing crosses a border, and the cross-region charges never appear because there's no second region to cross to. You're not balancing the axes anymore; they've all pointed at the same place.

## The short version

"EU or US hosting" isn't one decision, it's three. Latency is physics: put the origin near your users, and know that a CDN speeds up static assets but not the dynamic requests that reach your server. Law is about stability: US hosting of EU data leans on transfer frameworks with a record of being struck down, while EU residency removes the question. Cost is about borders, not per-gigabyte rates: a single-region stack avoids the cross-region, currency, and compliance charges a split one racks up. Answer the three questions for your own app, and if they all point to Europe, the simplest way to honour that is a stack that never leaves it. [Deploy in an EU region](https://runsite.app/services/web-services) and the region dropdown stops being a gamble.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is EU hosting slower than US hosting?

Only if your users are in the US. Latency is set by physical distance: roughly one millisecond of one-way delay per 200 kilometres of fibre, so a transatlantic round trip costs about 80 to 90 milliseconds before any processing. For European users, an EU origin is faster; for American users, a US origin is faster. A CDN caches static assets close to everyone, but it can't cache dynamic requests like logins, searches, or database writes, which still travel to the origin. The rule is to host near the majority of your users, whichever continent that is.

### Is US hosting cheaper than EU hosting?

Not in a way that usually decides it. Raw compute costs are broadly similar between reputable EU and US hosts. What actually moves the bill is borders: cross-region data transfer and egress, currency and VAT friction for a business billed in dollars, and the recurring compliance overhead (transfer impact assessments, Standard Contractual Clauses, subprocessor audits) that comes with moving EU data to the US. A single-region stack avoids most of those, so it's often cheaper in total even when the per-gigabyte compute price looks the same.

### Is it legal to host EU user data in the US?

Yes, but it counts as a transfer of personal data outside the EU under GDPR's Chapter V, which needs a legal basis. Since the 2020 Schrems II ruling invalidated Privacy Shield, that means either the recipient is certified under the EU-US Data Privacy Framework (the 2023 adequacy decision) or you rely on Standard Contractual Clauses plus a transfer impact assessment. It's legal today, but it depends on frameworks with a history of being struck down, and the US CLOUD Act can compel US providers to disclose data regardless of storage region. This isn't legal advice. Treat it as context for a conversation with a lawyer.

### Does hosting in the EU improve GDPR compliance?

It removes the single hardest and least stable part — international transfers — but it isn't compliance by itself. Keeping every copy of the data in the EU means there's no transfer to justify and no framework to monitor. You still need a lawful basis for processing, a way to handle data-subject requests, adequate security, and a Data Processing Agreement. Residency is one important piece of the picture, not the whole of it.

Keep reading

---

# Schrems II Explained: What It Means for EU Data Transfers

> Schrems II invalidated Privacy Shield in 2020, the second EU-US data bridge to fall. What it ruled, why frameworks keep collapsing, and how EU residency avoids it.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Schrems II and Data Transfers: Why an EU-Only Stack Is the Stable Answer

ComplianceJuly 15, 202610 min read

# Schrems II and Data Transfers: Why an EU-Only Stack Is the Stable Answer

Schrems II struck down Privacy Shield in 2020 — the second EU-US data bridge to collapse in five years. Here's what the ruling actually said, why these frameworks keep failing, and why keeping data in the EU sidesteps the entire cycle.

## The short version

Schrems II struck down Privacy Shield in 2020 — the second EU-US data bridge to fall in five years — because the conflict between GDPR and US surveillance law is structural rather than fixable by drafting. The 2023 Data Privacy Framework is already being challenged. Keeping EU data in the EU leaves no transfer to bet on.

[Teo Marquardt](https://runsite.app/about#author)

If you sell software into Europe, "Schrems II" turns up in places you didn't invite it. It's a line in a Data Processing Agreement, a checkbox in a vendor security review, the reason a prospect's lawyer asks where your database lives. Two words, usually left unexplained, quietly deciding whether a deal moves forward. Most of what's written about it is either lawyer-grade or fear-grade, and neither helps you make a hosting decision.

So here's the plain-terms version: what the ruling actually said, why it wasn't a one-off, and why it ends up being a question about where you host rather than what you sign. None of this is legal advice. It's the context that makes the term stop being a scary phrase in a contract and start being a decision you can reason about.

## Who Max Schrems is, and what got struck down

Max Schrems is an Austrian privacy lawyer who, as a student in 2011, asked Facebook for a copy of the data it held on him and got back a stack of pages that made him wonder what US law actually allowed once European data landed there. That question became a decade of litigation. His first case reached the Court of Justice of the European Union in 2015 and took down Safe Harbor, the agreement that had let companies move personal data across the Atlantic since 2000\. The framework that replaced it, Privacy Shield, is the one his second case was aimed at.

That second case, formally [C-311/18](https://curia.europa.eu/juris/liste.jsf?num=C-311/18) and known everywhere as Schrems II, was decided in July 2020\. The Court ruled that Privacy Shield did not give European data the protection GDPR requires once it reaches the United States, and struck it down with immediate effect. It left Standard Contractual Clauses standing as a transfer tool, but added a condition that changed what using them involves: you can't just sign them and assume the data is protected. That single caveat is what turned a court ruling into a workload for thousands of companies, and we'll get to it.

| Framework              | In force     | Ended by                     |
| ---------------------- | ------------ | ---------------------------- |
| Safe Harbor            | 2000–2015    | Schrems I (C-362/14)         |
| Privacy Shield         | 2016–2020    | Schrems II (C-311/18)        |
| Data Privacy Framework | 2023–present | In force, already challenged |

Three EU-US transfer bridges in twenty years. Two were struck down in court; the third is under the same pressure.

Read down that table and the shape of the thing is hard to miss. Two arrangements built to legalise the same transfer, both invalidated by the same court within five years of each other, each replaced by something structurally similar. This isn't a story about one bad framework getting fixed. It's a pattern, and understanding why it repeats tells you more than any single ruling does.

## Why the bridges keep collapsing

The reason is a conflict that no framework has resolved, because it isn't a paperwork problem. GDPR says personal data leaving the EU has to keep a level of protection that is "essentially equivalent" to what it had inside the Union. US surveillance law, in the Court's assessment, doesn't clear that bar. Programmes authorised under Section 702 of FISA and Executive Order 12333 give US intelligence agencies access to data held by US providers, and a European whose data gets swept up has no meaningful way to challenge it in a US court. As long as those two facts sit side by side, any bridge built over the gap is standing on ground the Court has already called unstable.

The [US CLOUD Act](https://www.congress.gov/bill/115th-congress/house-bill/4943) sharpens the point. It lets US authorities compel a US-based provider to produce data it controls no matter which country the servers are in. So a US company's European data centre doesn't fully settle the question, because the obligation follows the company, not the hardware. This is the part worth saying clearly, because it's where the fear-grade writing takes over: none of this is about US companies behaving badly or wanting to hand your data over. It's a straightforward clash between one legal system's guarantees and another's powers, and it's structural, which is exactly why patching the contract on top never quite holds.

The conflict in one sentence

GDPR promises European data a level of protection that US government-access law, in the Court's view, can't match — so every legal bridge built across that gap inherits the same weakness, and can be knocked down the same way the last one was.

## What Schrems II asked of everyone else

The headline was that Privacy Shield died, but the part that reached into everyday engineering was quieter. By keeping Standard Contractual Clauses alive only on the condition that they actually deliver equivalent protection, the Court made SCCs necessary but no longer sufficient. Signing the clauses stopped being the end of the task and became the start of it: you now have to check whether the law in the destination country would undercut what you just signed, and do something about it if it would.

That check has a name, a Transfer Impact Assessment, and the follow-on work has guidance behind it in the European Data Protection Board's [Recommendations 01/2020](https://www.edpb.europa.eu/our-work-tools/our-documents/recommendations/recommendations-012020-measures-supplement-transfer%5Fen) on supplementary measures. We walked through where these obligations come from in [where to store EU user data](https://runsite.app/blog/gdpr-data-residency); the point here is the ongoing cost of them. Every transfer of EU personal data to the US now carries a recurring chore list:

* Map which data actually leaves the EU, and to whom, including subprocessors your provider uses further down the chain.
* Assess whether US law would compromise the protection your SCCs promise, and write that assessment down.
* Add supplementary measures where it would — extra encryption, pseudonymisation, contractual and technical controls — and confirm they're enough.
* Keep the whole thing current, because the assessment expires the moment the legal ground shifts, which it does.

For a small team, that's real time spent on lawyers and documentation rather than product, and it doesn't buy certainty. It buys a defensible position that has to be re-defended whenever the framework underneath it moves again.

## The 2023 Data Privacy Framework, and why it isn't case closed

In July 2023 the European Commission adopted a new adequacy decision, the [EU-US Data Privacy Framework](https://commission.europa.eu/law/law-topic/data-protection/international-dimension-data-protection/eu-us-data-privacy-framework%5Fen), which restored a cleaner route for transfers to the US. It's genuine relief: if the US organisation you're sending data to has self-certified under the framework, you can rely on that instead of running the full SCC-plus-assessment gauntlet. For a lot of companies, the day-to-day friction eased considerably.

The catch is history. This is the third arrangement of its kind, built to bridge the same gap the last two couldn't hold. Schrems and his organisation noyb said publicly they consider it flawed on the same grounds as Privacy Shield and signalled they'd challenge it, a case people already call "Schrems III" before it has a number. As of writing the framework is valid and usable, and it may well survive. But relying on it means betting that this bridge outlasts the next court case, on a route where the previous two didn't. That's a fine bet to make with eyes open. It's a bad one to make by accident.

## The durable answer: don't have a transfer

Step back from the frameworks and the assessments and the acronyms, and notice that all of them exist to solve one problem: European data went somewhere GDPR doesn't reach cleanly. If the data never leaves, the problem never starts. There's no transfer to justify, no impact assessment to write, no framework to monitor, and no exposure to whatever the next ruling decides. The most stable answer to the transfer question turns out to be not having a transfer at all.

This is where residency stops being a nice-to-have and becomes the low-drama engineering choice. It's also worth being honest about its limits, the same way [EU vs US hosting](https://runsite.app/blog/eu-vs-us-hosting) is: keeping data in the EU removes the hardest and least stable part of compliance, but it isn't compliance by itself. You still need a lawful basis for processing, a way to handle data-subject requests, real security, and a [Data Processing Agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa). Residency takes the Schrems question off the table. It doesn't take the rest of GDPR off the table. The wider picture of what "hosting in Europe" involves is laid out in the [EU hosting guide](https://runsite.app/blog/eu-hosting-for-developers).

## What this means for your stack

In practice, sidestepping the transfer question means keeping every copy of the data on one side of the border, which is easier when the platform is built that way rather than configured into it after the fact. That's the setup Runsite runs on. [Web Services](https://runsite.app/services/web-services) keep your app containers, environment variables, build artifacts, and logs in a German region, with no replication to non-EU regions and no fallback to overseas infrastructure. Put [managed PostgreSQL](https://runsite.app/services/postgresql) and [S3-compatible object storage](https://runsite.app/services/s3-storage) next to them and the database, its backups, and your files sit in the same region rather than scattered across two continents.

The quiet advantage there is what's missing. There's no second region on another continent for data to replicate into, so there's no transfer to assess, no supplementary measures to bolt on, and nothing that a future Schrems ruling can reach. A signed GDPR Data Processing Agreement comes with every plan, so the contractual side is answered up front rather than negotiated as an enterprise add-on. The Schrems question doesn't get a better answer here; it just never gets asked.

## The short version

Schrems II struck down Privacy Shield in 2020, the second EU-US data bridge to fall in five years, because European data in the US can't get the protection GDPR demands while US surveillance law works the way it does. That conflict is structural, which is why the 2023 Data Privacy Framework, the third bridge, is already being challenged. Relying on any transfer framework means betting it survives the next case. Keeping EU data in the EU means there's no transfer to bet on: no assessment, no framework to watch, nothing for the next ruling to touch. It isn't the whole of compliance, but it's the one part you can settle permanently. If your users and your obligations are both in Europe, the simplest way to answer the Schrems question is to make sure it never comes up. [Keep the whole stack in an EU region](https://runsite.app/services/web-services) and it won't.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is Schrems II in simple terms?

Schrems II is a 2020 ruling by the Court of Justice of the European Union (case C-311/18) that struck down Privacy Shield, the framework companies used to move personal data from the EU to the US. The Court found that US surveillance law doesn't give European data the protection GDPR requires, and that a European whose data is accessed has no effective way to challenge it in the US. It kept Standard Contractual Clauses as a transfer tool but said signing them isn't enough on its own — you also have to assess whether the destination country's law undermines them. It's named after Max Schrems, the privacy lawyer who brought the case.

### Is Schrems II still relevant after the 2023 Data Privacy Framework?

Yes. The 2023 EU-US Data Privacy Framework restored a legal route for transfers to self-certified US organisations, which eased the day-to-day burden Schrems II created. But it's the third such framework, built to bridge the same gap the previous two couldn't hold, and Max Schrems's organisation has already signalled it will challenge it — a case commentators call "Schrems III." As of writing the framework is valid, but relying on it means betting it survives the next court case. The underlying conflict Schrems II identified hasn't gone away, so the reasoning behind it is still very much relevant.

### Do I need a Transfer Impact Assessment?

If you transfer EU personal data to a country without an EU adequacy decision and rely on Standard Contractual Clauses, then yes — Schrems II requires you to assess whether that country's law would undermine the protection the clauses promise, and to add supplementary measures if it would. The European Data Protection Board's Recommendations 01/2020 describe how. If you send data to a US organisation certified under the Data Privacy Framework, you can rely on that instead. And if the data never leaves the EU, there's no transfer to assess in the first place. This isn't legal advice; treat it as context for a conversation with a lawyer.

### How does hosting in the EU relate to Schrems II?

Schrems II is entirely about what happens when EU personal data is transferred out of the EU. If you keep every copy of the data inside the EU, there's no transfer, so the whole apparatus — impact assessments, supplementary measures, framework monitoring — simply doesn't apply, and no future Schrems-style ruling can reach your setup. Hosting in an EU region removes the hardest and least stable part of GDPR compliance. It doesn't remove the rest: you still need a lawful basis, data-subject processes, security, and a DPA. Residency settles the transfer question permanently, not the whole of compliance.

Keep reading

---

# Managed PostgreSQL for Developers: What's Actually Covered

> The operational jobs a managed PostgreSQL host takes over — provisioning, backups, pooling, upgrades, scaling — and the ones that stay yours whatever you pick.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Managed PostgreSQL for Developers: What the Host Actually Takes Off Your Plate

DatabasesJuly 17, 202611 min read

# Managed PostgreSQL for Developers: What the Host Actually Takes Off Your Plate

Installing Postgres takes a minute. Running it in production for three years is the real job. Here's the specific list of operational chores that move to the host when a database is "managed" — and the ones that stay yours no matter what.

## The short version

Managed PostgreSQL is a boundary, not a feature. Provisioning, backups, point-in-time recovery, pooling, upgrades, scaling, monitoring and durability move to the host. Your schema, indexes, queries and the connections your code opens stay with you, because those are the application. Choosing managed decides which side of that line your scarce hours go.

[Teo Marquardt](https://runsite.app/about#author)· Updated August 7, 2026

You can have PostgreSQL running in about a minute. `apt install postgresql`, or a single line in a Docker compose file, and there's a database listening on 5432\. That part was never the hard bit. The hard bit is the three years after that minute: the upgrade you keep postponing, the backup nobody has tested, the 2am page when connections run out. "Managed" is the word for handing that part to someone else, and it's worth knowing exactly what's inside it before you decide it's worth paying for.

Because "managed" isn't a single feature you can point at. It's a list of operational jobs that quietly move from your side of the line to the host's. This guide walks that list one item at a time: what each job is, what it costs when you own it, and where managed stops and your own work begins (the part most vendor pages skip). None of it is magic. It's just a question of whose weekend gets ruined when something breaks.

## What "managed" actually means

The clearest way to define managed is by contrast. An unmanaged database is one where you own the whole server: the operating system, the Postgres process, the disk it writes to, the config file, the backups, the monitoring, and every upgrade for the life of the machine. It's yours down to the kernel. That's a fine choice, and sometimes the right one, but it means the database is now a system you operate, not just a thing you query.

A managed database draws the line higher up. The host owns everything beneath your data: the machine, the Postgres binary, the replication, the backup pipeline, the patch schedule. You own the data and everything you do to it: the schema, the queries, the indexes. You connect with a URL and write SQL; the layer underneath is somebody else's responsibility. This is what the acronyms are pointing at when a provider calls itself database as a service, or DBaaS. Everything below is a tour of what sits in that underneath layer, so the trade you're making is concrete rather than a vibe.

## The chores that move off your plate

Here's the operational work a managed provider absorbs. If you've only ever used a database through an ORM, some of these are jobs you didn't know existed, which is rather the point. They don't announce themselves until the day they go wrong.

### Provisioning and day-one setup

A raw Postgres install ships with defaults tuned for a laptop from a decade ago. Getting it production-ready means sizing `shared_buffers` and `work_mem` to the machine, setting up TLS so connections aren't in plaintext, creating roles with sane permissions, and locking down `pg_hba.conf` so the whole internet can't knock on the door. A managed instance arrives with this already done and tuned to the hardware it runs on. On Runsite that's a PostgreSQL 16 database you spin up in seconds, with TLS on by default, and the whole setup checklist is somebody else's checklist.

### Backups and point-in-time recovery

Everyone agrees backups matter, right up until they find out theirs haven't run since March. Doing this properly is more than a nightly `pg_dump` to the same disk the database lives on, because that disk is exactly what fails. You want full snapshots stored somewhere else, plus continuous archiving of the [write-ahead log](https://www.postgresql.org/docs/current/wal-intro.html) so you can rewind to any second, not just to last midnight. That last capability, [point-in-time recovery](https://runsite.app/blog/postgres-point-in-time-recovery), is what turns an accidental `DROP TABLE users` at 3pm into a ten-minute restore instead of a resignation letter.

Managed handles both halves. Runsite takes a daily snapshot and streams WAL segments continuously, keeping recovery points for 7 to 30 days depending on plan, and a restore is picking a timestamp in the dashboard rather than a tense evening with the docs. If you want a second copy under your own control on top of that (belt and suspenders), you can [automate Postgres backups to S3](https://runsite.app/blog/postgresql-backup-to-s3) and keep a portable dump in your own bucket. Managed backups are the host's promise; a copy you hold is yours to keep.

### Connection pooling

Every Postgres connection is a real operating-system process with real memory behind it, so the server can only hold so many before it tips over. A busy web app with a handful of instances, each opening its own pool, blows through the limit fast, and the reward is `FATAL: sorry, too many clients already` in the logs while the site throws 500s. The standard fix is a pooler like [PgBouncer](https://www.pgbouncer.org/) sitting in front, multiplexing thousands of client connections onto a small, steady set of real ones. Running that yourself is one more process to deploy, tune, and keep alive.

Managed providers usually bundle the pooler in. Runsite runs PgBouncer built in, so you connect on port 6432 for the pooled endpoint instead of 5432 for a direct one, and the multiplexing happens without a second service to babysit. How the pooling modes differ and what each port actually promises is its own topic, covered in [connection pooling with PgBouncer](https://runsite.app/blog/pgbouncer-connection-pooling). It's not a cure-all, either, and you can still exhaust a pool by leaking connections in your own code, which is a different problem we'll come back to.

### Version upgrades and security patches

Postgres ships a new major version every year and patches the supported ones regularly, some of those patches closing real security holes. Upgrading a major version by hand is genuinely fiddly. It can mean a dump-and-restore or a logical replication dance, with downtime to plan around, so on a self-run box it's the maintenance that slips quarter after quarter until you're stranded on a version that stopped getting security fixes two years ago. A managed platform handles patching and offers a supported upgrade path, so "we're on an unsupported version" stops being a sentence you say in a post-mortem.

### Scaling and headroom

Traffic grows, and the box that was comfortable in January is wheezing by June. Self-hosted, adding CPU, memory, or disk means a maintenance window and a migration, and running out of disk mid-day is a genuine outage, since Postgres does not cope gracefully with a full volume. Managed turns this into a slider. Runsite does one-click vertical scaling for CPU and memory, and storage autoscaling so the disk grows before it fills rather than after. For read-heavy workloads, read replicas on the Business plan spread query load across more than one machine, which self-hosting can do too, at the cost of building and monitoring the replication yourself.

### Monitoring and high availability

You can't fix what you can't see, so a production database needs eyes on connection counts, slow queries, memory pressure, disk headroom, and replication lag. Self-hosted, that's a Prometheus exporter, a dashboard, and alert rules you assemble and maintain. Managed gives you the metrics out of the box: Runsite surfaces connections, queries per second, memory, storage, and replication lag in a dashboard from the start. Underneath, redundant snapshots and replication mean a single failed disk isn't the end of your data, which is the sort of durability engineering that's tedious to get right and easy to get subtly wrong on your own.

| The chore             | Cost if you self-host                      | What managed does                                 |
| --------------------- | ------------------------------------------ | ------------------------------------------------- |
| Provisioning & tuning | Hours of config, TLS, roles per instance   | Arrives tuned, TLS on, ready to query             |
| Backups & PITR        | Build & test WAL archiving; hope it works  | Daily snapshots + continuous WAL, 1-click restore |
| Connection pooling    | Deploy & tune PgBouncer yourself           | Pooler built in on a dedicated port               |
| Upgrades & patches    | Manual, downtime, easy to postpone forever | Patched for you, supported upgrade path           |
| Scaling               | Maintenance window + migration per change  | One-click resize, storage autoscaling             |
| Monitoring & HA       | Wire up exporters, dashboards, replication | Metrics dashboard + redundant snapshots included  |

The managed database trade, one row at a time: the work doesn't vanish, it moves to someone whose full-time job it is.

## Where managed stops

This is the part the brochures leave out, and it's the most useful thing to be clear about. Managed hosting gives you a healthy server. It does not give you a healthy schema. Everything above the data line is still entirely your job, and no amount of managing changes that.

* Your schema and data model are yours: how the tables relate, what you normalize, which constraints you enforce. A managed host will faithfully run a bad schema at full speed.
* Indexes are on you too. The host won't know that your busiest query does a sequential scan over two million rows because the column it filters on was never indexed.
* Slow queries and N+1 are code problems. An ORM that fires 300 queries to render one page doesn't get fixed by a bigger machine; it just gets expensively fast.
* Connections you leak stay your bug. Pooling raises the ceiling, but code that opens connections and never returns them will still hit it. Managed gives you the pooler, not the discipline.

That last one is worth sitting with, because it's the most common way people blame the database for something they're doing to it. If you've ever hit connection limits despite a pooler, the cause is usually in the application, and the walkthrough in [too many clients already](https://runsite.app/blog/postgres-too-many-clients-already) traces it back to source. The honest framing: managed takes the operating-a-server job off your plate completely, and leaves the using-a-database-well job exactly where it was. That's not a gap in the service. It's the line between infrastructure and your application, and it's supposed to be there.

The one-line version of the boundary

A managed provider is responsible for keeping the database up, safe, and current. You're responsible for what you ask it to do: the schema, the queries, and the connections your code opens. Managed fixes the server; it can't fix the SQL.

## Does that list justify the price?

Every job above is one you can do yourself on a cheap VPS, which is why the sticker price makes self-hosting look like the obvious win. The database bill was never the expensive part, though — the engineer's evening, the 2am page, and the week that goes into a major-version upgrade are. Whether that trade favours you depends on numbers specific to your team, so rather than hand-wave it here: [self-hosted vs managed PostgreSQL](https://runsite.app/blog/self-hosted-vs-managed-postgresql) prices each job by the hours it costs, adds the risk that never reaches an invoice, and lays out the cases where self-hosting genuinely still wins.

## What to look for in a managed provider

Every major cloud has a managed Postgres offering now, from AWS RDS and Google Cloud SQL to newer entrants like Supabase and Neon, and they don't all draw the line in the same place. It's worth checking what's actually included versus what's an upsell. A short checklist to run any provider against:

* Point-in-time recovery, not just nightly dumps, with a retention window long enough to notice a problem before the backup rolls off.
* Connection pooling included, not a separate thing you deploy.
* The extensions you actually use, like PostGIS for geospatial, [pgvector for embeddings](https://runsite.app/blog/pgvector-embeddings-postgresql), and pg\_trgm for fuzzy search, supported rather than blocked.
* A scaling story that doesn't need a migration: vertical resize and storage autoscaling at minimum.
* Encryption at rest and in transit as defaults, not toggles you have to remember to switch on.
* Clear data residency and a Data Processing Agreement if you handle EU users, so compliance isn't an afterthought.
* A way to try it without a sales call or a credit card, so you can see the shape of it before committing.

## How Runsite handles it

For a concrete example of where the line sits, [managed PostgreSQL](https://runsite.app/services/postgresql) on Runsite covers the operational layer described above and gets out of the way of your SQL. You get a PostgreSQL 16 instance in seconds, daily backups with point-in-time recovery from continuous WAL archiving, PgBouncer pooling built in on port 6432, one-click vertical scaling with storage autoscaling, read replicas on Business, and a monitoring dashboard for the metrics that matter. The extensions people reach for, PostGIS, pgvector, and pg\_trgm, are all there — [storing and searching AI embeddings with pgvector](https://runsite.app/blog/pgvector-embeddings-postgresql) works without a second system — and connections run over private networking with TLS by default.

Because the platform is EU-native, the database, its WAL archive, and every backup stay in a German region and never cross an EU border, which is the residency question that [EU hosting for developers](https://runsite.app/blog/eu-hosting-for-developers) unpacks in full. A signed GDPR Data Processing Agreement comes with every plan, no upcharge and no sales call, and the entry Nano plan is a 1 GB database with daily backups at €3 a month, covered by the €5 of credit every new account starts with, no card required. Put [Redis](https://runsite.app/services/redis) in front to cache hot queries when read traffic climbs, and the pieces that talk to the database all sit in the same region.

## The short version

Managed PostgreSQL isn't a feature; it's a boundary. On the host's side of it sit provisioning, backups and point-in-time recovery, pooling, upgrades, scaling, monitoring, and durability — the operational work that costs you evenings and attention when you own it, and costs you a line item when you don't. On your side sit the schema, the indexes, the queries, and the connections your code opens, and no host can take those off your plate because they're the actual application. Choosing managed is deciding you'd rather spend your scarce engineering time above that line than below it. For most teams shipping a product rather than running a database company, that's the easy call. [Spin one up on the €3 Nano plan](https://runsite.app/services/postgresql), which the €5 starting credit covers, and you'll feel where the line is within an afternoon.

Related service

## PostgreSQL on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore PostgreSQL](https://runsite.app/services/postgresql)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does "managed PostgreSQL" actually include?

It covers the operational layer beneath your data: provisioning and tuning a production-ready instance, automated backups with point-in-time recovery, connection pooling, security patches and version upgrades, vertical and storage scaling, monitoring, and the replication that keeps a single hardware failure from losing data. You still own the schema, indexes, and queries — managed keeps the server healthy, not your SQL. On Runsite that includes PostgreSQL 16, daily backups with WAL-based point-in-time recovery, built-in PgBouncer pooling, one-click scaling, and a metrics dashboard.

### Is managed PostgreSQL worth it versus self-hosting on a VPS?

For most small teams, yes — but not because of the monthly price, where a cheap VPS looks cheaper. The real cost of self-hosting is the engineering time spent on backups, upgrades, pooling, monitoring, and 2am incidents, all of which is undifferentiated plumbing rather than product work. Managed converts that variable, attention-heavy cost into a predictable line item. Self-hosting still wins when you need an extension or kernel tweak no provider offers, operate at a scale where you employ database engineers anyway, or have a hard cost floor. If you're unsure which case you're in, you're almost certainly better served by managed.

### Does managed PostgreSQL mean I never have to think about my database?

No, and that's the most important thing to understand about it. Managed hosting takes over operating the server — keeping it up, backed up, patched, and scaled. It does not design your schema, add your indexes, or fix a query that scans two million rows because the column it filters on was never indexed. It also can't stop your code from leaking connections and exhausting the pool. Managed gives you a healthy server; a healthy database on top of it is still your job.

### Can I still use extensions like PostGIS and pgvector on managed PostgreSQL?

On a good managed provider, yes — the extensions people commonly reach for are supported rather than blocked. Runsite supports PostGIS for geospatial queries, pgvector for storing and searching AI embeddings, and pg\_trgm for fuzzy text matching, alongside PostgreSQL's built-in full-text search. Extension support does vary between providers, though, so if you depend on a specific or unusual one, it's worth confirming it's available before you migrate.

Keep reading

---

# PgBouncer Pooling: Port 5432 vs 6432 Explained

> How PgBouncer pools PostgreSQL connections, why the direct 5432 and pooled 6432 ports differ, the three pooling modes, and what breaks in transaction mode.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Connection Pooling with PgBouncer: What Ports 5432 and 6432 Really Do

DatabasesJuly 20, 202610 min read

# Connection Pooling with PgBouncer: What Ports 5432 and 6432 Really Do

You switched to port 6432 to stop the connection errors, and now prepared statements throw and SET doesn't stick. That's pooling working as designed. Here's what a pooler actually does, why 5432 and 6432 are different contracts, and what breaks in transaction mode.

## The short version

PostgreSQL connections are processes behind a hard ceiling, so a pooler reuses a small warm set instead of opening one per request. Ports 5432 and 6432 are different contracts: 6432 for high-concurrency app traffic in transaction mode, 5432 for anything needing session state — migrations, LISTEN/NOTIFY, advisory locks, prepared statements you cannot disable.

[Teo Marquardt](https://runsite.app/about#author)

You added `?pgbouncer=true` to your connection string, moved the port from 5432 to 6432, and the `too many clients already` errors stopped. Then something else broke: a prepared statement throws an error it never used to, or a `SET search_path` you rely on quietly stops taking effect. Nothing is actually wrong. That's connection pooling doing exactly what it's designed to do, and the surprises come from switching it on without knowing what it changes underneath.

This is the concept behind the fix. If you're staring at the error itself right now, the walkthrough in [too many clients already](https://runsite.app/blog/postgres-too-many-clients-already) is the fire extinguisher. This piece is the other half: what a pooler actually does, why the pooled 6432 port and the direct 5432 port are two different contracts rather than two spellings of the same thing, and which of your session habits stop working once traffic runs through PgBouncer.

## Why Postgres needs a pooler at all

PostgreSQL uses a process-per-connection model. Every open connection is a real operating-system process on the server with its own memory for query plans, buffers, and a work area, held whether the connection is running a query or sitting idle. That's a sturdy design, and it's also why the [max\_connections limit](https://www.postgresql.org/docs/current/runtime-config-connection.html) exists: a few hundred mostly-idle connections can eat the RAM the database needs to actually run queries.

Opening a connection isn't free either. There's a TCP handshake, a TLS negotiation, authentication, and backend startup on every one, so an app that opens a fresh connection per request pays that tax thousands of times a minute for work a handful of reused connections could do. A pooler exists to solve both problems at once: keep a small, warm set of real connections open and let many clients take turns on them instead of each dialing its own.

## Two layers of pooling, and why you may need both

Pooling happens at two levels, and they cover different halves of the problem.

The first is the pool inside your application, shipped with every serious database driver. It keeps a few connections open per process and hands them to requests instead of dialing a new one each time. This is the first thing to reach for, and [too many clients already](https://runsite.app/blog/postgres-too-many-clients-already) covers how to size it. It has one hard limit: an app-side pool only knows about its own process. Run four instances that each cap at 20 connections and you've authorized 80, plus whatever your workers and cron jobs open, with nothing coordinating that total against the database ceiling.

The second level is an external pooler that sits in front of the database, with every client connecting through it. Because all traffic funnels through one place, it can enforce a fleet-wide limit no app-side pool can see. This is the layer that earns its keep when you scale horizontally or run serverless functions, where the instance count is elastic and each cold start would otherwise open its own connections. PgBouncer is the standard tool for the job, and it's what the 6432 port is.

## What PgBouncer actually does

[PgBouncer](https://www.pgbouncer.org/) is a lightweight proxy that speaks the PostgreSQL wire protocol. Your app connects to PgBouncer as if it were the database; PgBouncer keeps its own small set of real connections to Postgres and multiplexes incoming clients onto them. A thousand clients can be "connected" to PgBouncer while only, say, twenty real backend connections exist behind it. It works because most connected clients aren't doing anything at any given instant, so a small shared set is enough to serve them without each one holding a backend process hostage.

How aggressively that sharing happens comes down to one setting: the pooling mode.

## The three pooling modes: session vs transaction vs statement

PgBouncer offers three modes, and they differ on a single question: how long does a client get to keep a real connection before it goes back into the shared set?

| Mode        | Server connection is held for                  | Good for                                               | What you give up                                                                  |
| ----------- | ---------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- |
| Session     | The whole client session, until it disconnects | Long-lived connections that need full session features | Most of the pooling benefit — it behaves close to a direct connection             |
| Transaction | A single transaction, then it's returned       | Web and API traffic with short transactions            | Anything spanning transactions: session state, prepared statements, LISTEN/NOTIFY |
| Statement   | A single statement                             | Very high concurrency, autocommit-only workloads       | Multi-statement transactions, which it forbids outright                           |

The three PgBouncer pooling modes. Transaction mode is the usual choice for web apps, and it's also the one with the footguns.

In session pooling, a client holds its assigned backend connection from connect to disconnect. None of your session behaviour changes, which makes it the safest mode, but it also means one idle client still ties up one real connection. You get the connection reuse without much of the multiplexing.

Transaction pooling is where the benefit lives, and it's the mode most web apps want. A client is handed a real connection only for the duration of a transaction; the instant that transaction commits or rolls back, the connection goes back to the pool for someone else. Between transactions your client is connected to PgBouncer but holds no backend at all. This is how a handful of real connections serve a large fleet, and it's almost certainly what a managed `pgbouncer=true` endpoint gives you.

Statement pooling goes one step further and reclaims the connection after every single statement, which rules out multi-statement transactions entirely. It's a niche mode for specific autocommit workloads, not something you'd turn on by accident.

## Port 5432 vs 6432: two doors, different contracts

Here's the mental model that makes the rest click. Port 5432 and port 6432 are not two databases, and not two URLs for convenience. They're two doors into the same database, and each door comes with a different contract about what you're allowed to assume.

Port 5432 is the direct line to Postgres. You get a real backend process to yourself for the whole session, so every session-level feature works the way the manual says. Port 6432 goes through PgBouncer, typically in transaction mode, so you get a connection only for each transaction and must not assume anything survives between them. Same data, same queries, different rules about session state.

| Task                                   | Port | Why                                                                               |
| -------------------------------------- | ---- | --------------------------------------------------------------------------------- |
| Regular app request traffic            | 6432 | Short transactions at high concurrency — exactly what transaction pooling is for  |
| Database migrations and schema changes | 5432 | Often need session state, advisory locks, or steps that run outside a transaction |
| LISTEN / NOTIFY                        | 5432 | Needs a persistent session that transaction pooling won't keep                    |
| Session advisory locks                 | 5432 | A session lock has to be released by the same session that took it                |
| A psql session for debugging           | 5432 | You want a stable session, not a connection that changes under you                |

A rough division of labour: 6432 for app traffic, 5432 for the occasional job that needs a stable session.

## What transaction pooling breaks (and how to fix it)

Every gotcha in transaction pooling traces back to one root cause: your client can get a different backend connection for the next transaction, so anything that expected to live on one connection across transactions won't be there. These are the ones that bite in practice.

* **Prepared statements.** Many drivers prepare a statement once and reuse the plan, which assumes the same backend is still there next time. Under transaction pooling it may not be. As of writing, recent PgBouncer versions (1.21 and later) can [track prepared statements across the pool](https://www.pgbouncer.org/config.html#max%5Fprepared%5Fstatements) when configured, but plenty of managed setups still expect you to disable client-side prepared-statement caching. Each driver has a knob for it — Prisma has `pgbouncer=true`, asyncpg has `statement_cache_size=0`, the JDBC driver has `prepareThreshold=0` — and the exact setting for yours belongs in the [Runsite docs](https://docs.runsite.app) rather than guessed at mid-incident.
* **Session-level `SET`.** Running `SET search_path = ...` or `SET timezone = ...` on its own sets state on whatever backend you happened to hold, and the next transaction may land elsewhere without it. Use `SET LOCAL` inside the transaction that needs it, so the setting is scoped to that transaction and travels with the work.
* **`LISTEN` / `NOTIFY`.** These depend on a connection that stays put and keeps listening. Transaction pooling recycles the connection out from under them, so run pub/sub-style listeners on the direct 5432 port.
* **Session advisory locks.** `pg_advisory_lock()` takes a lock tied to the session and expects the same session to release it. Through a transaction pooler the session isn't stable, so the lock can outlive the client that took it and wedge everyone else. Use the transaction-scoped variant `pg_advisory_xact_lock()`, or take these on 5432.
* **Temporary tables and `WITH HOLD` cursors.** Both are session-scoped and won't reliably survive to the next transaction on a pooled connection. If you depend on them, put that work on a direct session.

The rule that covers all of them

Transaction pooling only promises you a connection for the length of a transaction. If a feature needs state to persist between transactions on the same physical connection, it either wants a LOCAL or transaction-scoped variant, or it wants the direct 5432 port. When in doubt, ask one question: does this assume the same connection is still here next time?

## Sizing the pool: smaller than you'd guess

The instinct with pools is to make them big, on the theory that more connections means more throughput. Past a point it's the reverse. Every active backend is a process competing for the same cores and disk, so a pool far larger than the machine can run in parallel just adds contention and context-switching, and queries slow down under load instead of speeding up.

A better starting point is a small pool sized to the database's actual CPU and disk, not to your traffic. A pooler in transaction mode keeps those few connections busy by cycling them between transactions quickly, which is the whole idea: you don't need one backend per client, you need enough to keep the CPU saturated and no more. Start conservative, watch for clients spending time waiting on a free connection, and raise the size only if the database has the headroom to use them. A big number on a small instance buys instability, the same trap as raising `max_connections` to paper over a leak.

## How Runsite handles it

Running PgBouncer yourself means another process to deploy, configure, and keep alive next to the database, and it's one line on the [real cost of self-hosting Postgres](https://runsite.app/blog/self-hosted-vs-managed-postgresql). [Managed PostgreSQL](https://runsite.app/services/postgresql) on Runsite has PgBouncer built in, so there's no separate pooler to operate. The direct database is on port 5432 and the pooled endpoint is on 6432, chosen by a single flag on the connection string:

bash

```
# Direct session — every session feature works (port 5432)
DATABASE_URL=postgresql://user:pass@db.runsite.app:5432/mydb

# Pooled through PgBouncer — for app request traffic (port 6432)
DATABASE_URL=postgresql://user:pass@db.runsite.app:6432/mydb?pgbouncer=true
```

The usual pattern is to point your application's normal traffic at 6432 and keep 5432 for migrations, admin tasks, and anything that needs a stable session. Because the platform is EU-native, the database, its WAL archive, and every backup stay in a German region with a signed GDPR DPA on every plan, so reaching for the pooled endpoint doesn't move any of your data — it's the same database behind both ports. Pooling is one of the operational jobs that [managed PostgreSQL is meant to take off your plate](https://runsite.app/blog/managed-postgresql-for-developers), and this is what that looks like in practice.

## The short version

A pooler exists because PostgreSQL connections are expensive processes behind a hard ceiling, and reusing a small warm set beats opening a new one per request. A cache in front of the database changes how many of those connections you need, and [the caching pattern you pick decides whether it changes them on reads, on writes, or on both](https://runsite.app/blog/cache-aside-write-through-write-behind). PgBouncer does that in one of three modes, and transaction mode — the common one for web apps — hands you a real connection only for each transaction. That's why the pooled 6432 port and the direct 5432 port are different contracts: 6432 for high-concurrency app traffic, 5432 for anything that needs session state, whether that's a migration, `LISTEN/NOTIFY`, a session advisory lock, or prepared statements you can't disable. Match the port to the work and pooling stops being a source of mystery errors. A pooler makes the connections you have go further; the other way to get further is to stop issuing the query at all, which is what [a cache in front of the database](https://runsite.app/blog/when-to-use-redis-cache) does for reads that repeat.

You shouldn't have to run the pooler to get its benefit. [Spin up a managed PostgreSQL database](https://runsite.app/services/postgresql) with PgBouncer already wired to the 6432 endpoint, and point your app at pooling that someone else keeps alive.

Related service

## Managed PostgreSQL on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed PostgreSQL](https://runsite.app/services/postgresql)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What's the difference between port 5432 and 6432 on PostgreSQL?

Port 5432 is the standard direct connection to PostgreSQL — you get a real backend process for your whole session, and every session-level feature works normally. Port 6432 is the common convention for a pooled endpoint through PgBouncer, usually in transaction mode, where you're handed a real connection only for the length of each transaction. Both point at the same database. Use 6432 for high-concurrency application traffic and 5432 for migrations, admin tasks, and anything that relies on session state like LISTEN/NOTIFY or session advisory locks.

### What is transaction pooling in PgBouncer?

Transaction pooling is the PgBouncer mode where a client is assigned a real database connection only for the length of a single transaction. As soon as that transaction commits or rolls back, the connection returns to the shared pool for another client. It's the mode most web apps use, because it lets a small set of real connections serve a large number of clients. The tradeoff is that anything relying on session state persisting between transactions — prepared statements, session-level SET, LISTEN/NOTIFY — can't be assumed to work without adjustment.

### Why do prepared statements break with PgBouncer?

Many drivers prepare a statement on one connection and reuse it, which assumes that connection is still assigned to them next time. Under transaction pooling it usually isn't — the next transaction may land on a different backend that never saw the prepared statement. As of writing, recent PgBouncer versions (1.21 and later) can track prepared statements across the pool when configured, but many setups still expect you to disable client-side prepared-statement caching. Each driver has a setting for it, so check your driver's docs for the exact flag.

### Do I still need an app-side connection pool if I use PgBouncer?

Usually yes, because they do different jobs. An app-side pool avoids the cost of opening a new connection for every request within a single process. PgBouncer enforces a limit across your whole fleet — every instance, worker, and cron job — which an app-side pool can't see because it only knows its own process. For a horizontally scaled or serverless app, you want the driver pool for per-process reuse and PgBouncer in front to keep the total under the database's ceiling.

Keep reading

---

# Point-in-Time Recovery in PostgreSQL, Explained

> How PostgreSQL point-in-time recovery works, what the write-ahead log stores, how far back you can rewind, and what PITR can't undo after a bad migration.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Point-in-Time Recovery: How WAL Archiving Saves You After a DROP TABLE

DatabasesJuly 22, 202610 min read

# Point-in-Time Recovery: How WAL Archiving Saves You After a DROP TABLE

Your nightly backup answers "do we have a copy?" Point-in-time recovery answers "can we go back to 3:06pm?" Here's what the write-ahead log stores, how PITR replays it, and what it still can't undo.

## The short version

A nightly backup answers whether you have a copy. Point-in-time recovery answers whether you can return to 3:06pm, by replaying archived write-ahead log segments onto a base backup. It rebuilds a whole database rather than a row, reaches back only as far as the retention window, and lives inside your provider's account.

[Teo Marquardt](https://runsite.app/about#author)· Updated August 8, 2026

A migration runs at 3:07pm and takes a table with it. Someone notices at 3:20\. The first question isn't whether you have a backup, because you do — it ran at 2am like it does every night. The question is what happened to the database between 2am and 3:07pm, and whether any of it is coming back.

That gap is the difference between having backups and having recovery. Point-in-time recovery closes it, and it works by reusing something PostgreSQL already does on every single write for an entirely different reason. Below: what the write-ahead log really is, how a restore replays it to rebuild your database as it stood at a specific second, and the limits nobody mentions until you hit one.

## Why a nightly backup isn't the answer on its own

A backup taken once a day sets a floor on how much data you can lose: everything written since it ran. Restore last night's dump after an afternoon incident and you get the database as it was at 2am, minus a full day of orders and edits that nobody has a copy of anymore. The backup worked perfectly. It just wasn't a recovery plan.

Disaster recovery has a term for that ceiling: the recovery point objective, or RPO. It's how much data you're willing to lose, measured in time. Nightly dumps give you an RPO of up to 24 hours. Its sibling, the recovery time objective (RTO), is how long you're willing to be down while you put things back. Most teams have never written either number down, and then discover both of them on the worst possible afternoon.

Point-in-time recovery drops the RPO from a day to seconds without asking you to take a snapshot every second. It does that by keeping the record of every change the database made in between.

## What the write-ahead log actually is

PostgreSQL doesn't write your change straight into the data files. Before a modification touches a table's pages on disk, it's appended to the [write-ahead log](https://www.postgresql.org/docs/current/wal-intro.html): a sequential record of what changed, flushed to durable storage before the transaction is allowed to report success, at least under the default commit settings. The data files catch up later, in the background, at a checkpoint.

This exists for durability, not for you. If the server loses power mid-transaction, the data files may be half-updated, but the WAL is a complete account of what was supposed to happen. On restart, Postgres replays the log against the files and arrives at a consistent state. That's crash recovery, and every PostgreSQL install does it whether or not anyone is thinking about backups.

[Continuous archiving](https://www.postgresql.org/docs/current/continuous-archiving.html) takes that same log and copies each completed segment somewhere durable instead of letting it be recycled. Once the log is being kept rather than discarded, the mechanism that recovers from a crash also recovers from a mistake. A snapshot is a photograph of the database at one moment. The archived WAL is the recording of everything that happened after the shutter closed.

One caveat on how fine-grained that recording is. WAL is archived a segment at a time, 16 MB by default, so a plain archive setup on a quiet database ships a segment only when it fills up. Getting close to a seconds-level worst case depends on the log being streamed continuously or forced to rotate on a timer, which is a detail managed platforms handle and self-hosted setups often miss — one of several lines in the [real cost of self-hosting Postgres versus managed](https://runsite.app/blog/self-hosted-vs-managed-postgresql).

## How point-in-time recovery works

PITR needs two ingredients: a base backup, which is a physical copy of the database files taken at some point in the past, and an unbroken stream of archived WAL covering the time since. Restoring means starting from the base backup and replaying archived log records on top of it, one after another, until you reach the moment you asked for. Then replay stops. By default Postgres pauses there rather than opening straight away, so you get a chance to look at the state and confirm you landed where you meant to before promoting it.

The thing worth internalising is that recovery runs forward, not backward. Nothing is being undone. The database is being rebuilt from an older state by replaying real history up to a chosen cut-off, which is why it can land on any second inside the window and why the result is a consistent database rather than a table with a hole patched in it. That also shapes the standard procedure, and what managed platforms do by default: the restore goes into a new instance sitting at that timestamp, and your damaged one stays untouched. Which is exactly what you want when you're not yet sure what went wrong.

Don't fix production in place

The instinct after a bad write is to repair the live database. Resist it. Every write you make now is more WAL between you and a clean recovery point, and if you overwrite the damaged rows you lose the evidence of what actually happened. Restore into a separate instance and compare.

## Recovery targets: time isn't the only one

"Point in time" is the common case, not the only option. PostgreSQL lets you [tell recovery where to stop](https://www.postgresql.org/docs/current/runtime-config-wal.html#RUNTIME-CONFIG-WAL-RECOVERY-TARGET) in several ways, and the alternatives matter when a wall-clock timestamp isn't precise enough:

* **A timestamp** (`recovery_target_time`) — what you reach for when you know roughly when the damage happened. Note the default: recovery stops just \*after\* the target commit, and `recovery_target_inclusive = off` is what makes it stop before. If the target is the bad transaction itself, you want it off.
* **A transaction ID** (`recovery_target_xid`) — pin the cut-off to one transaction when a timestamp would sweep up innocent neighbours that committed in the same second. Transaction IDs are handed out when a transaction starts, so numeric order isn't always commit order.
* **An LSN** (`recovery_target_lsn`) — a precise position in the log stream, for when you're working from log analysis rather than a clock.
* **A named restore point** (`recovery_target_name`) — a label created with `pg_create_restore_point()` before doing something risky, which gives you a bookmark you can say out loud during an incident. It's superuser-only by default, so on a managed platform check whether it's available to you before planning around it.

Left unset, recovery simply replays everything available and ends at the newest record in the archive. That default is what you want for hardware failure, where you're recovering from a dead machine and want every committed transaction back. It's the wrong default for a logical mistake, where the mistake itself is faithfully recorded in the log and will be replayed right along with everything else.

ini

```
# The shape of a recovery target in postgresql.conf.
# Managed platforms set this for you when you pick a timestamp.
recovery_target_time = '2026-07-22 15:07:00+02'
recovery_target_inclusive = off   # default is 'on' — stop *after* the target
```

On a managed database you generally don't write these lines. You pick a moment in a dashboard and the platform assembles the base backup, the WAL, and the target for you. Knowing what's underneath is still worth it, because it tells you what to ask for when the clock matters.

## Snapshot, WAL, and a logical dump do different jobs

People use "backup" for all three of these, which is how you end up with a recovery plan that has a hole in it. They're separate objects with separate strengths.

| What it is                      | Granularity                         | Best RPO                | Portable?                                                          | What it's for                                   |
| ------------------------------- | ----------------------------------- | ----------------------- | ------------------------------------------------------------------ | ----------------------------------------------- |
| Base backup (physical snapshot) | The whole cluster, as of one moment | Since the last snapshot | No — same major version and architecture                           | The starting point PITR replays onto            |
| Archived WAL                    | Every committed change, in order    | Seconds                 | No — paired with its base backup                                   | Rewinding to an exact moment                    |
| Logical dump (pg\_dump)         | A database, schema, or single table | Since the last dump     | Yes — restores into the same or a newer major version, on any host | Long-term archives and moving between providers |

Physical backups plus WAL give you precision. A logical dump gives you portability. Neither substitutes for the other.

The portability row is the one that catches people. A base backup is a byte-level copy of a PostgreSQL cluster and expects to be restored by the same major version on compatible hardware. A plain-format `pg_dump` file is SQL text, so it loads into the same or a newer major version, on a laptop or another provider entirely. That's why a second, portable copy is worth keeping alongside the platform's snapshots, and why [automating a dump to your own S3 bucket](https://runsite.app/blog/postgresql-backup-to-s3) solves a different problem from the one PITR solves.

## What point-in-time recovery can't do

### It isn't an undo button for one table

Recovery works at the level of the whole cluster, every database in the instance, not the row. Rewinding to 3:06pm gives you every table as it stood at 3:06pm, including the twenty minutes of legitimate writes that landed elsewhere before anyone noticed. If you restore over production, you've traded a dropped table for lost orders. Worth knowing too if staging and production share an instance, because they rewind together.

The workable version is a side restore: bring up a copy at the target timestamp, pull the table you lost out of it with `pg_dump -t`, and load it into the live database. If you'd rather reconcile in SQL than move files around, `postgres_fdw` can bridge the two instances so you can query the old state directly. Slower than a one-click rollback, and much easier to explain afterwards.

### The retention window is a real deadline

PITR reaches back as far as the oldest base backup with an unbroken WAL archive running forward from it, typically a rolling window of days or weeks. Inside the window you can go to any second. Outside it there's nothing to replay. This is fine for the incidents you notice in minutes and useless for the ones you notice in quarters: a subtly wrong `UPDATE` from six weeks ago, a background job that has been writing bad values since a deploy nobody connected to it. By the time the report looks wrong, the correct state has aged out.

### It lives in the same account as everything else

The snapshots and the WAL archive sit inside your provider. That's the right place for them, and it means they share a fate with the account they live in. Losing access, whether through a billing dispute or a lockout, takes the recovery option with it. That's the case for keeping one copy somewhere you control, which is the whole argument in [automating PostgreSQL backups to S3](https://runsite.app/blog/postgresql-backup-to-s3).

## The first ten minutes after a bad write

Recovery goes better when the first few decisions are already made. A rough order of operations:

1. **Stop the writes.** Pause the job, revert the deploy, or take the affected feature offline. Every additional write makes the eventual reconciliation harder.
2. **Write down the time.** The last known-good moment and the moment of damage, in a specific timezone. You'll be asked for one of them by a dashboard shortly, and memory is unreliable twenty minutes into an incident.
3. **Work out the blast radius.** One table or several? Deleted rows or overwritten values? This decides whether you extract one table from a copy or roll the whole database back.
4. **Restore to a new instance, not over the old one.** Target a timestamp a little before the damage. The damaged database stays available for comparison.
5. **Verify before you migrate anything.** Count rows, spot-check the records people complained about, confirm you landed before the bad transaction rather than after it.
6. **Move the good data across,** then write down what happened while it's fresh. The postmortem is where a retention window or a missing alert gets fixed.

None of this requires special tooling. It requires that continuous archiving was already running before the incident, which is the part you can't arrange retroactively.

## How Runsite handles it

Setting up continuous archiving yourself means configuring an archive command, storing segments somewhere durable, monitoring for gaps in the stream, and periodically proving that a restore actually works. It's a real piece of infrastructure, and its failure mode is silence.

[Managed PostgreSQL](https://runsite.app/services/postgresql) on Runsite runs it as part of the service. WAL segments stream continuously to durable storage, a full snapshot is taken daily during quiet hours, and recovery points are kept for 7 to 30 days depending on plan. Restoring means picking a timestamp in the dashboard, and Runsite provisions a new database holding your data as it was at that second — the side restore described above, without assembling it by hand.

A WAL archive and a snapshot are complete copies of your data, so where they sit matters as much as where the database does. Both stay in the German region, which means a restore never pulls data across a non-EU boundary, and it's the half of the residency question that [where to store EU user data](https://runsite.app/blog/gdpr-data-residency) covers in full. The pillar on [which database chores a host actually operates](https://runsite.app/blog/managed-postgresql-for-developers) lists durability as one line in a longer inventory of operational work; this is what that one line actually contains.

## What to remember

PostgreSQL writes every change to the write-ahead log before it touches the data files, for crash safety. Archive that log instead of recycling it and the same mechanism becomes a time machine: take a base backup, replay the archived WAL on top of it, and restore to a specific time by stopping at the second you name. That's point-in-time recovery, and it takes your worst-case data loss from "since last night's backup" down to seconds.

It rebuilds a database, not a row, so recovering one table means restoring a copy and lifting the data out. It reaches back only as far as the retention window. And it lives in your provider's account, which is why a portable dump in a bucket you own is still worth keeping. [Start a managed PostgreSQL database](https://runsite.app/services/postgresql) with continuous WAL archiving already running, and the option to rewind is there before you need it rather than after.

Related service

## Managed PostgreSQL on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed PostgreSQL](https://runsite.app/services/postgresql)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is point-in-time recovery in PostgreSQL?

Point-in-time recovery (PITR) restores a PostgreSQL database to its exact state at a chosen moment. It combines a base backup, which is a physical copy of the database files, with continuously archived write-ahead log segments covering the time since. Recovery starts from the base backup and replays log records forward until it reaches the target you specify, then opens the database there. That lets you land on any second inside your retention window rather than only on the moment your last backup ran.

### Can point-in-time recovery restore a single dropped table?

Not directly. PITR rebuilds the whole database as it stood at the target moment, so restoring in place would also roll back every legitimate write that happened after the table was dropped. The standard approach is a side restore: bring up a separate instance at a timestamp just before the damage, extract the missing table from it, and copy that data into the live database. Managed platforms usually restore to a new instance by default, which is exactly what this procedure needs.

### What is the write-ahead log (WAL) in PostgreSQL?

The write-ahead log is a sequential record of every change to the database, written and flushed to durable storage before the change is applied to the data files themselves. Its original purpose is crash recovery: after an unclean shutdown, PostgreSQL replays the log to bring the data files back to a consistent state. When those log segments are archived rather than recycled, the same record can be replayed onto an older backup to reconstruct the database at any moment it covers.

### How often should I back up PostgreSQL?

Daily is the common default for a full backup, but the frequency of full backups isn't what sets your worst case — continuous WAL archiving is. A daily backup on its own means losing up to 24 hours of writes if you have to restore. A daily base backup plus continuously archived WAL means losing seconds, because the log fills in everything that happened between snapshots. Set the schedule from how much data you can afford to lose, then keep a separate logical dump for anything that has to outlive the retention window.

### Does point-in-time recovery replace regular backups?

No, because it depends on them — PITR replays WAL on top of a base backup, so the backup is half of the mechanism. It also doesn't cover everything a separate backup does. Recovery only reaches back as far as your retention window, typically days or weeks, so damage discovered months later is out of reach. And because the snapshots and WAL archive live in your provider's account, losing access to that account takes them with it. A periodic logical dump in storage you control covers both gaps, since it restores across versions and providers.

Keep reading

---

# pgvector: AI Embeddings and Vector Search in Postgres

> What pgvector adds to PostgreSQL: the vector type, distance operators, HNSW vs IVFFlat indexes, and when Postgres beats a dedicated vector database.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. pgvector: Storing and Searching AI Embeddings in PostgreSQL

DatabasesJuly 24, 202611 min read

# pgvector: Storing and Searching AI Embeddings in PostgreSQL

Your app called an embeddings API and got back 1,536 numbers per document. Before you add a vector database to the stack: what pgvector stores, how its two indexes trade recall for speed, and when Postgres alone carries a semantic search feature.

## The short version

An embedding is a point in space, and every AI search feature reduces to finding the nearest points. pgvector teaches the database you already run to answer that query: exact and index-free while the data is small, fast and approximate through HNSW once it grows. Vectors beside their rows buy SQL filters and one residency story.

[Teo Marquardt](https://runsite.app/about#author)

Semantic search made it onto the roadmap, you called an embeddings API, and every document in your product now comes with an array of 1,536 floating-point numbers. The numbers are the easy part. The question is where to put them, and the reflex answer — spin up a dedicated vector database — adds a second stateful system to your stack before anyone has checked whether the database you already run can do the job.

Usually it can. pgvector is the PostgreSQL extension that teaches the database to store embeddings and search them by similarity, and for most applications it turns "we need a vector database" into "we need one more column". This piece covers what an embedding looks like from the database's side, what pgvector actually adds, the exact-versus-approximate trade behind its two index types, and the honest version of when a dedicated vector engine earns its place.

## What an embedding is, from the database's point of view

An embedding is what you get when a model reads a piece of content — a paragraph, a support ticket, a product description — and encodes its meaning as a list of numbers. Each list is a point in a space with hundreds or thousands of dimensions, and the model's one useful promise is that similar meanings land near each other. "How do I reset my password" and "can't log into my account" become points that sit close together, even though they share almost no words.

For the database, that changes the shape of the question. Classic search asks "which rows contain this word?", a job for text indexes and `WHERE` clauses. Semantic search asks "which rows are nearest to this point?", which is a nearest neighbor query. Every feature in this family reduces to that one operation: semantic search ranks documents by distance to the query's embedding, recommendations rank items by distance to things the user liked, and retrieval-augmented generation (RAG) fetches the nearest chunks of your content and hands them to a model as context. Store the points, measure distances quickly — that's the entire requirement.

## What pgvector actually adds

[pgvector](https://github.com/pgvector/pgvector) is an open-source extension that adds a `vector` column type to PostgreSQL, distance operators to compare vectors, and index types to make those comparisons fast at scale. Enabling it is one statement, and after that a vector column behaves like any other column: it lives in an ordinary table, next to ordinary types.

sql

```
CREATE EXTENSION vector;

CREATE TABLE documents (
  id bigserial PRIMARY KEY,
  title text NOT NULL,
  body text NOT NULL,
  embedding vector(1536)
);

-- The ten most similar documents, by cosine distance
SELECT id, title
FROM documents
ORDER BY embedding <=> $1
LIMIT 10;
```

The `<=>` in that query is cosine distance, the measure most embedding providers recommend. pgvector also ships `<->` for Euclidean (L2) distance and `<#>` for negative inner product (negated so that closer still sorts first), and recent releases keep adding more: as of writing, version 0.7.0 brought an L1 operator plus compact half-precision and sparse vector types. For normalized embeddings, which is what most current models return, the different measures produce the same ranking anyway, so the practical advice is short: use `<=>` unless your model's documentation says otherwise.

That's most of the contract. One thing pgvector deliberately doesn't do: it never calls an embeddings API for you. Your application computes the query's embedding, passes it as a parameter, and gets rows back ordered by similarity, with all of SQL wrapped around the result. The step-by-step wiring for a specific framework belongs in the docs; the point here is that nothing about the shape is exotic.

## Exact search first: you may not need an index

The query above runs on a plain table with no index at all. Postgres computes the distance to every row and sorts — a sequential scan, exact and complete. Recall is 100% by definition: the ten nearest rows it returns are truly the ten nearest rows.

What's under-appreciated is how far that carries. Scanning tens of thousands of vectors takes milliseconds, and a few hundred thousand often still lands within an interactive budget on reasonable hardware. If your corpus is a documentation site, a product catalogue, or one tenant's data at a time, you may live in exact-search territory for a long while, with nothing to tune and nothing to rebuild. The right time to add an index is when latency tells you to, not when the architecture diagram would look more serious with one.

The order of operations

Start with the bare column and exact search, and measure. Add an index when a real query on real data is too slow. An approximate index spends recall to buy speed, and that's a trade worth making deliberately rather than by default.

## HNSW vs IVFFlat: the approximate index trade

Past the point where scanning everything is too slow, you add an approximate nearest neighbor (ANN) index, and the word approximate is doing real work. An ANN index doesn't check every row; it organizes the vectors so a search can visit a promising fraction of them and skip the rest. That's what makes million-row similarity queries fast. It's also why recall drops below 100%: occasionally the true nearest neighbor sits in a region the search didn't visit. Every vector database makes the same trade. It can be tuned, but it can't be avoided.

pgvector ships two index types, and they organize the space differently.

|                       | HNSW                                                                          | IVFFlat                                                                  |
| --------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| How it works          | A multi-layer graph of neighbors, walked from an entry point toward the query | Vectors clustered into lists; the search scans only the closest clusters |
| Build cost            | Slower to build, more memory                                                  | Faster to build, lighter                                                 |
| Query speed vs recall | The better curve, as of writing                                               | Good, but usually behind HNSW at equal recall                            |
| Empty table?          | Fine — the graph grows with inserts                                           | Needs data first: it clusters what's there at build time                 |
| Tuning knobs          | m, ef\_construction at build; hnsw.ef\_search per query                       | lists at build; ivfflat.probes per query                                 |
| Choose it when        | Query speed and recall matter most — the common case                          | Build time or memory is the constraint, and data loads once              |

pgvector's two approximate index types. As of writing, HNSW is the usual default recommendation.

sql

```
-- HNSW, on cosine distance (matches the <=> operator)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

-- IVFFlat: build it after the table has data in it
CREATE INDEX ON documents
  USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);
```

If in doubt, HNSW — added in pgvector 0.5.0 — costs more up front and holds the better speed-to-recall curve at query time. IVFFlat keeps a niche where the index has to build fast or fit in less memory, with one operational caveat: because it derives its clusters from whatever data exists at build time, an index created on a nearly empty table clusters badly. Bulk-load first, then build, and rebuild after the data changes shape.

One version-sensitive limit to check before you commit to an embedding model. As of writing, the `vector` type stores up to 16,000 dimensions, but the indexes support up to 2,000 (4,000 for the half-precision `halfvec` type). A 1,536-dimension embedding — the size of OpenAI's text-embedding-3-small, as of writing — indexes without workarounds. A 3,072-dimension one needs half-precision, a model option that shortens the output, or exact search with no index. These numbers move between releases, so check the pgvector README against your model of choice.

## The real advantage: vectors next to your data

Benchmarks get the attention, but the strongest argument for pgvector isn't about milliseconds. It's that the embeddings live in the same database as the rows they describe, and a separate system has to reimplement everything that follows from that.

* **Filters are just SQL.** "Nearest documents this user may see, in this workspace, published this year" is a `WHERE` clause and a `JOIN` bolted onto the similarity query. A dedicated engine needs its own filter syntax, fed with metadata you copied over and must keep current.
* **Updates are transactions.** Edit a document and its embedding in one transaction, and search never sees a row whose vector describes last week's text. Across two systems, that consistency becomes your application's problem.
* **There's no sync pipeline.** A second store means change capture, retries, backfills, and reconciliation — plumbing whose failure mode is stale search results nobody notices for a week.
* **One operational envelope.** Backups, recovery, monitoring, and residency are already answered for your database. A second stateful system answers them all again.

That last point compounds quietly. Embeddings are derived data, but they're often derived from the most sensitive content you hold, so where they're stored and how they're backed up isn't a footnote. Inside Postgres they're covered by the same [point-in-time recovery](https://runsite.app/blog/postgres-point-in-time-recovery) that protects everything else, and they inherit whatever data-residency answer you've already given — the question [where to store EU user data](https://runsite.app/blog/gdpr-data-residency) unpacks. A separate vector store is a second copy of that homework.

## When a dedicated vector database wins

There is an honest boundary. Pinecone, Qdrant, Weaviate, Milvus and their peers are good systems built around exactly one workload, and there are cases where they earn the second system:

* **Scale.** As a rough order of magnitude as of writing: single-node Postgres with pgvector is comfortable through millions of vectors. When a collection heads toward hundreds of millions, engines with sharding and quantization designed in from the start justify themselves.
* **Vector search is the product.** If high-QPS similarity queries are the core workload rather than one feature among many, a specialized engine's tuning surface and horizontal scaling become worth operating.
* **Specialized features, natively.** Built-in hybrid search pipelines, multi-vector queries, aggressive quantization options. Some of this exists as Postgres extensions; if you need all of it, you're describing a dedicated engine.

Notice what's not on the list: adding semantic search to an existing product. That's the common case, and it's the one where reaching for a second database first is premature. The migration path is forgiving, too. If you start in Postgres and genuinely outgrow it, your embeddings are rows in a table, and exporting them to a specialized engine is a batch job rather than an archaeology project. Starting with two systems on day one pays the operational cost immediately, in exchange for a scaling ceiling you may never touch.

| Your situation                                                    | A reasonable default                        |
| ----------------------------------------------------------------- | ------------------------------------------- |
| Adding semantic search or RAG to an app whose data is in Postgres | pgvector, exact search, no index yet        |
| Up to millions of vectors, latency starting to matter             | pgvector with an HNSW index                 |
| Hundreds of millions of vectors, or search is the core product    | A dedicated vector database                 |
| Strict recall and latency targets at high QPS                     | Benchmark both on your data before deciding |

A decision frame, not a law: the numbers are order-of-magnitude guides as of writing.

## How Runsite handles it

On [managed PostgreSQL](https://runsite.app/services/postgresql), pgvector is one of the pre-installed extensions, alongside PostGIS and pg\_trgm, so enabling it is the same `CREATE EXTENSION vector;` from earlier — no support ticket, no custom image. The database is PostgreSQL 16 by default, and the extension is available from the free 1 GB tier up, which is enough to prototype a semantic search feature against real data before paying anything.

The operational layer a second database would have doubled stays single. Daily backups and continuous WAL archiving cover vector columns like any others, similarity queries ride the built-in [PgBouncer pooled endpoint](https://runsite.app/blog/pgbouncer-connection-pooling) as ordinary app traffic, and the whole envelope — data, embeddings, WAL archive, backups — stays in the German region with a signed GDPR DPA on every plan. Running the database itself is the part [managed PostgreSQL takes off your plate](https://runsite.app/blog/managed-postgresql-for-developers); pgvector means the vector workload comes along without adding a new system to run.

## The short version

An embedding is a point in space, and every AI-adjacent search feature reduces to finding the nearest points. pgvector teaches the database you already run to answer that query: exactly and index-free while the data is small, fast and approximate through an HNSW index once it grows. Keeping vectors next to the rows they describe buys you SQL filters, atomic updates, and one backup-and-residency story instead of two. A dedicated vector database is real infrastructure for real cases — hundreds of millions of vectors, search as the core product — and the way to find out whether you're such a case is to outgrow Postgres, not to assume in advance that you will.

If your application's data already lives in Postgres, the shortest path to vector search is a database with pgvector waiting behind a single `CREATE EXTENSION`. [Spin up managed PostgreSQL](https://runsite.app/services/postgresql) on the €3 Nano plan, which the €5 starting credit covers, and the first similarity query is an afternoon away.

Related service

## Managed PostgreSQL on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed PostgreSQL](https://runsite.app/services/postgresql)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is pgvector?

pgvector is an open-source PostgreSQL extension that adds a vector column type for storing embeddings, distance operators (cosine, Euclidean, inner product) for comparing them, and two index types — HNSW and IVFFlat — for fast approximate nearest neighbor search. It turns a regular Postgres database into a working vector store: embeddings sit in ordinary tables, similarity search is an ORDER BY clause, and all of SQL, including joins, filters, and transactions, applies to them. On providers that ship the extension, it's enabled with a single CREATE EXTENSION vector; statement.

### Do I need a dedicated vector database, or is PostgreSQL enough?

For the common case — adding semantic search, recommendations, or RAG to an application whose data already lives in Postgres — pgvector is usually enough, and it spares you operating and synchronizing a second stateful system. As a rough guide as of writing, single-node Postgres handles collections in the millions of vectors comfortably. A dedicated vector database earns its place when collections head toward hundreds of millions, when high-QPS vector search is the core product, or when you need specialized features like native hybrid search pipelines. Starting in Postgres keeps the migration path open: embeddings are rows in a table, and exporting them later is a batch job.

### What's the difference between HNSW and IVFFlat indexes in pgvector?

Both are approximate nearest neighbor indexes; they differ in structure and trade-offs. HNSW builds a multi-layer graph of neighbors: it's slower to build and uses more memory, but delivers the better query speed-to-recall curve, and it can be created on an empty table because the graph grows with inserts. IVFFlat clusters vectors into lists and searches only the closest clusters: it builds faster and uses less memory, but needs data present at build time to cluster well, and typically trails HNSW at the same recall. As of writing, HNSW is the usual default unless build time or memory is your binding constraint.

### Can pgvector handle OpenAI embedding dimensions?

Yes, with one limit to check. As of writing, pgvector's vector type stores up to 16,000 dimensions, but its indexes support up to 2,000 — or 4,000 with the half-precision halfvec type. OpenAI's text-embedding-3-small produces 1,536-dimension embeddings as of writing, which index without any workarounds. text-embedding-3-large defaults to 3,072 dimensions, which exceeds the standard index limit; options include the model's dimensions parameter to shorten the output, half-precision indexing, or exact search without an index. These limits change between pgvector releases, so check the current README before committing to a model.

Keep reading

---

# Self-Hosted vs Managed PostgreSQL: The Real Cost

> The €5 VPS looks cheaper than the €14 managed plan until you price the hours. The real cost of self-hosting Postgres vs managed, and when to self-host.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Self-Hosted vs Managed PostgreSQL: The Real Cost of Ownership

DatabasesJuly 27, 202611 min read

# Self-Hosted vs Managed PostgreSQL: The Real Cost of Ownership

The €5 VPS looks cheaper than the €14 managed plan — until you price the hours. A line-by-line cost of ownership for self-hosting Postgres versus managed, the risk nobody puts on the invoice, and when self-hosting actually wins.

## The short version

The sticker price is the easy number and the wrong one to decide on. Self-hosting wins the monthly bill and loses total cost of ownership, because it moves two invisible costs onto you: the tens of hours a year of operating a database well, and the expected cost of the failures you now own.

[Teo Marquardt](https://runsite.app/about#author)

Open your provider's pricing page and the comparison looks settled before you've finished reading it. A small VPS you could run Postgres on yourself is five euros a month. A managed PostgreSQL plan with similar specs is fourteen. Nearly three times the price for a database you already know how to install. `apt install postgresql` is free, after all, and you've done it a dozen times.

That top-line number is real, and it's also the wrong number to decide on. The monthly bill for a database was never the expensive part of running one. The expensive part doesn't show up on any invoice: it's the hours you spend keeping it alive, and the cost of the one night it isn't. This is an attempt to put those two hidden numbers on the table next to the visible one, so the comparison is honest. If you want the concept of what "managed" even covers, [the inventory of jobs a database host takes over](https://runsite.app/blog/managed-postgresql-for-developers) walks that boundary; this piece is about what each side of it costs.

## The two bills: sticker price and everything else

Every piece of infrastructure has two prices. The first is the sticker price: the line item on your monthly bill, the compute and storage you pay for whether or not anything goes wrong. It's easy to compare because it's a single number, which is exactly why it dominates the decision and exactly why it misleads.

The second price is the total cost of ownership, and it's the one that actually empties the account. For a database, TCO is the sticker price plus two things that never appear on the invoice: the engineering time it takes to operate, and the expected cost of the failures you're now responsible for. Self-hosting trades a lower first number for a higher, lumpier second one. Managed does the reverse. The whole decision is which trade fits you, and you can't see it until both numbers are written down.

## The self-hosted ledger, line by line

So write the second number down. Here's the operational work a database needs over a year when you own the whole server, with a rough sense of how often each job lands and how long it takes. The hours are illustrative, not a quote (your numbers depend on your experience and how much goes wrong), but the shape holds across most teams.

### Setup and hardening (once, then again per environment)

A fresh Postgres install ships with defaults tuned for a laptop, listening with settings you wouldn't run in production. Getting it ready means sizing memory parameters to the box, turning on TLS, creating roles that aren't the superuser, and locking down `pg_hba.conf` so the open internet can't reach it. Done carefully, that's the better part of a day the first time, and a chunk of one again for staging, for the replica, and for the rebuild after you outgrow the first box.

### Patching and minor upgrades (a few times a year)

PostgreSQL ships minor releases regularly, and some of them close real security holes. Applying one is rarely dramatic, but it's a maintenance window you plan, a change you test, and a release you have to actually notice came out. Skip the noticing and you're quietly running a version with a known vulnerability in it.

### The annual major-version upgrade (once a year, and it's the big one)

Postgres puts out a [new major version every year](https://www.postgresql.org/support/versioning/), and old ones fall out of support on a schedule. Upgrading across a major version by hand is the fiddly one: often a dump-and-restore or a logical-replication dance, with downtime to plan and a rollback to rehearse. It's the maintenance most likely to slip quarter after quarter until you're stranded two versions back with no more security fixes coming. When you finally do it, it eats a day or more of focused, nervous work.

### Backups you've actually restored (setup, then ongoing proof)

Configuring a nightly `pg_dump` is an hour. A backup you can trust is a different thing. You want copies stored somewhere other than the disk that might die, and continuous WAL archiving so you can rewind to a moment rather than to last midnight. Then the part everyone skips: a restore you've actually run, because a backup you've never restored is a hope, not a backup. That's the setup cost of doing [point-in-time recovery](https://runsite.app/blog/postgres-point-in-time-recovery) yourself, plus the recurring cost of proving it still works. If you want a portable copy on top, [automating backups to S3](https://runsite.app/blog/postgresql-backup-to-s3) is its own small pipeline to build and watch.

### Monitoring and alerting (build once, tend forever)

You can't fix what you can't see, so production needs eyes on connection counts, slow queries, memory pressure, disk headroom, and replication lag. Self-hosted, that's an exporter, a dashboard, and alert rules you assemble and then keep working as things change. Without it, the first sign of trouble is the outage itself.

### Pooling, replication, and the rest (as you grow)

Traffic grows and the single box stops being enough. Now you're standing up a [connection pooler](https://runsite.app/blog/pgbouncer-connection-pooling) so the app stops hitting the connection ceiling, and maybe a read replica with the replication lag to watch that comes with it. Each is another moving part you deploy, tune, and keep alive. The failure mode when you don't is a concrete one: [too many clients already](https://runsite.app/blog/postgres-too-many-clients-already) in the logs while the site throws 500s.

| The job                  | How often            | Rough hours / year | The details                         |
| ------------------------ | -------------------- | ------------------ | ----------------------------------- |
| Setup & hardening        | Once per environment | 6–10               | Tuning, TLS, roles, pg\_hba         |
| Minor patches            | A few times a year   | 3–6                | Test, schedule, apply               |
| Major upgrade            | Yearly               | 6–12               | Dump/restore or logical replication |
| Backups + tested restore | Setup + ongoing      | 6–10               | WAL, PITR, restore drills           |
| Monitoring & alerts      | Build + maintain     | 4–8                | Exporter, dashboard, rules          |
| Pooling / replication    | As you scale         | 4–10               | PgBouncer, read replicas            |

Illustrative, not a quote — but the total lands somewhere in the tens of hours a year for most single-database teams, before anything actually breaks.

## The cost nobody puts on the invoice

Add those up and you've still only counted the good years. The line that's hardest to see, and the one that most often decides the whole thing, is risk: the cost of the failures that self-hosting moves onto you. It doesn't arrive every month. It arrives once, at the worst possible time, and it can be larger than years of the sticker-price difference put together.

The honest way to price a risk is expected cost: how bad it would be, times how likely it is. Run down the list and the numbers get uncomfortable.

* **The backup you never tested.** Configured, running, green in the dashboard, and no good, because the one time you reach for it you find the restore doesn't work. Impact: potentially the whole database. Probability: far higher than anyone admits, because untested restores fail all the time.
* **The patch you didn't apply.** A known vulnerability in a version you meant to upgrade off months ago. Impact: a breach and everything that follows it. Probability: climbs the longer the upgrade slips.
* **The disk that filled up.** Postgres doesn't degrade gracefully when its volume is full; it stops accepting writes. Impact: a hard outage in the middle of the day. Probability: near certain, eventually, without headroom or autoscaling watching it.
* **The 2am page.** Connections exhausted, replication broken, a query storm — whatever it is, it's yours to fix now, not in the morning. Impact: the incident, plus the lost sleep, plus the next day gone. Probability: recurring.

Why the risk line dominates

A few euros a month of sticker-price savings is a rounding error next to one bad restore. If self-hosting saves you €100 a year and raises your chance of an unrecoverable data-loss event by even a couple of percent, the expected cost of that single risk can swallow the saving many times over. The saving is certain and small; the risk is uncertain and large. That asymmetry is the whole argument.

## Putting a number on your own time

The way to make this concrete is to price your own hours, because that's the currency self-hosting actually spends. Take what an hour of your time is worth — not just salary, but the loaded cost, and more honestly still, what that hour would earn spent on the product instead. Multiply by the hours a year the ledger above will cost you. Add a rough expected cost for the risk. That total is the real price of the cheap VPS.

text

```
Yearly cost of self-hosting =
    sticker price (VPS)
  + (hours per year  x  your loaded hourly cost)
  + (risk impact  x  probability)

Yearly cost of managed =
    sticker price (plan)
```

Put real numbers in and the result is usually lopsided. Say the ledger runs 30 hours a year and your time is worth €60 an hour, loaded. That's €1,800 of engineering attention spent on plumbing, before a single incident, to save the €100-ish a year the VPS is cheaper on sticker. The database was never the thing you were being paid to build, and those 30 hours came out of the thing you were.

## A three-year comparison

Stretch it over the life of an actual project and the gap widens, because the sticker-price saving is linear while the time and risk keep compounding. Here's the same small database run both ways for three years, with the managed side priced at a real plan (Runsite's Standard at €14 a month as of writing, which includes pooling, point-in-time recovery, and backups you don't operate) and the self-hosted side on a typical small VPS around €8 a month. The figures are illustrative; plug in your own.

| Cost line                | Self-host on a VPS | Managed (Standard plan)   |
| ------------------------ | ------------------ | ------------------------- |
| Sticker, 3 years         | \~€290 (≈€8/mo)    | \~€500 (€14/mo)           |
| Setup & hardening        | 6–10 hrs           | Minutes                   |
| Patching & upgrades      | \~9–18 hrs / year  | Included                  |
| Backups + tested restore | \~6–10 hrs / year  | Included, 1-click restore |
| Monitoring & pooling     | \~8–18 hrs / year  | Included                  |
| Your time over 3 years   | \~75–150 hrs       | \~0                       |
| That time at €60/hr      | \~€4,500–9,000     | €0                        |
| Risk exposure            | Yours              | The provider's            |

The \~€210 the VPS saves on sticker over three years is bought with 75–150 hours of your time and the full weight of every failure. The visible line is the small one.

The point isn't the exact figures, which will move with your rates and your luck. It's the direction: the number that decides the comparison is the one that isn't printed on either pricing page.

## When self-hosting actually wins

None of this makes managed the right call every time, and a comparison that only ran one way wouldn't be worth trusting. Self-hosting genuinely wins in a few specific situations, and if you're in one of them you probably already know it.

* **You need something no provider offers.** A particular extension, a kernel-level tweak, a custom build, or a version outside the supported range. If your workload depends on it, managed's tidy boundary becomes a wall, and owning the box is the only way through.
* **You already employ people who run databases.** At a certain scale you have a platform or SRE team keeping databases alive as their actual job. The operational hours are already staffed, so the ledger above is mostly sunk cost, and raw compute at volume can beat per-instance managed pricing.
* **You have a hard cost floor only raw compute can meet.** Some businesses genuinely need the cheapest possible per-unit infrastructure and have the in-house skill to run it safely. If the margin math forces it and the expertise is real, self-hosting is a defensible answer.

The break-even is really about which resource is scarcest. If engineering attention is your bottleneck (the usual case for a small team shipping a product), managed wins, because it converts a lumpy, attention-heavy cost into a flat line item. If cheap compute at scale is the bottleneck and operational skill is abundant, self-hosting wins. Most small teams sit firmly in the first camp, and if you're unsure which one you're in, you're almost certainly in it. The same ledger applies to the layer that deploys your code rather than the one that stores your data, and it comes out differently there, which is the subject of [self-hosted PaaS versus managed](https://runsite.app/blog/coolify-vs-managed-paas).

## How Runsite handles it

For a concrete version of the managed side of that trade, [managed PostgreSQL](https://runsite.app/services/postgresql) on Runsite prices the operations in rather than leaving them on your ledger. Every line the self-hosted column charges you hours for is already handled: a PostgreSQL 16 database provisioned and tuned in seconds, daily backups with point-in-time recovery from continuous WAL archiving, PgBouncer pooling built in on port 6432, one-click vertical scaling with storage autoscaling so the disk grows before it fills, and a monitoring dashboard for connections, queries, memory, and replication lag out of the box.

The plans map to that ledger directly. The entry Nano plan is a 1 GB database with daily backups at €3 a month that the €5 starting credit covers without a card, Starter at €5 adds connection pooling, Standard at €14 adds point-in-time recovery, and read replicas arrive on the larger plans when read traffic makes them worth it. Because the platform is EU-native, the database, its WAL archive, and every backup stay in a German region with a signed GDPR DPA on every plan, so the operational work moves off your plate without your data moving anywhere it shouldn't. What you're paying for isn't the compute, since a VPS sells that cheaper. It's the tens of hours a year and the risk exposure that never make it onto the invoice.

## The short version

The sticker price is the easy number to compare and the wrong one to decide on. Self-hosting Postgres wins on the monthly bill and loses on total cost of ownership, because it moves two invisible costs onto you: the tens of hours a year it takes to operate a database well, and the expected cost of the failures you're now on the hook for. Price your own time, price the risk honestly, and for most small teams the managed plan comes in cheaper than the cheap VPS. Self-hosting still wins when you need something no provider offers, already staff people to run databases, or have a hard compute floor — real cases, just not the common one. [Spin up a managed database on the €3 Nano plan](https://runsite.app/services/postgresql), covered by the €5 starting credit, and the operations are priced in before you write a line of SQL.

Related service

## Managed PostgreSQL on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed PostgreSQL](https://runsite.app/services/postgresql)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is managed PostgreSQL worth it, or is self-hosting cheaper?

On the monthly bill, self-hosting is usually cheaper — a small VPS you run Postgres on yourself can be a few times cheaper than a comparable managed plan. On total cost of ownership, managed usually wins. Self-hosting adds two costs that never appear on the invoice: the engineering hours to set up, patch, upgrade, back up, monitor, and pool the database (tens of hours a year for most teams), and the expected cost of the failures you're now responsible for, like an untested backup or a missed security patch. Price those in and the cheaper sticker price is often the more expensive option.

### What is the total cost of ownership of a database?

Total cost of ownership (TCO) is the full cost of running a database over its life, not just the monthly bill. It's the sticker price for compute and storage, plus the engineering time to operate it — provisioning, patching, major-version upgrades, backups with tested restores, monitoring, and connection pooling — plus the risk-adjusted cost of the failures the operator is responsible for. Managed hosting raises the sticker price and shrinks the other two; self-hosting does the reverse.

### How much time does it take to maintain a self-hosted PostgreSQL server?

For a single production database, plan on tens of hours a year once you count everything: initial setup and hardening, a few minor patches, the annual major-version upgrade, backups with restores you actually test, monitoring and alerting, and a pooler once traffic grows. The exact figure depends on your experience and how much goes wrong, but it's rarely the near-zero people assume when they compare it to a managed plan on price alone. That time comes out of building your product.

### When does self-hosting PostgreSQL make sense?

Self-hosting makes sense in three main cases: you need an extension, kernel tweak, or version no managed provider offers; you already employ a platform or SRE team that runs databases as their job, so the operational hours are already staffed; or you have a hard cost floor that only raw compute at scale can meet, along with the in-house skill to run it safely. Outside those, where engineering attention is the scarce resource — the usual situation for a small team shipping a product — managed almost always wins on total cost.

Keep reading

---

# S3-Compatible Object Storage: How It Works & How to Pick

> What object storage actually is, what "S3-compatible" really guarantees, where compatibility breaks, and how to compare providers before you migrate.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. S3-Compatible Object Storage: How It Works and How to Choose a Provider

StorageJuly 29, 202611 min read

# S3-Compatible Object Storage: How It Works and How to Choose a Provider

"S3" names three separate things: a product, a storage model, and an API. This guide separates them — how buckets actually behave, what "S3-compatible" guarantees, where that compatibility runs out, and the axes worth comparing providers on.

## The short version

Object storage is a different data model, not a cheaper disk: flat keys, objects written whole, metadata carried with the bytes, and listing that costs an API call. S3-compatible is a real guarantee about the wire protocol for core verbs — not for versioning, lifecycle, policies or events. Verify what you depend on against a live bucket.

[Teo Marquardt](https://runsite.app/about#author)

"Just put it in S3" is the advice everyone gives the moment an app starts handling files, and it's usually the right advice. What makes it confusing is that the letter-and-number names three separate things at once: Amazon's product, an object storage model that has very little in common with a filesystem, and an HTTP API that a dozen other providers now implement. When a storage service calls itself "S3-compatible," only the third of those is a promise, and it's worth knowing exactly what that promise covers before you build on it.

This guide separates the three. The model comes first: what a bucket actually is, and how it behaves differently from the disk you're used to. Then the API, including the specific places where compatibility stops being a guarantee. The last part is the comparison itself, the axes worth checking before you commit data to a provider, most of which never appear on a pricing page. The money side has an article of its own, so the numbers here stay light.

## What object storage actually is

There are three broad ways to keep bytes on someone else's hardware, and they differ in what you address rather than in how much they hold. Block storage hands you a raw device: numbered blocks, a filesystem laid on top, one machine attached at a time. That's what a database wants, because Postgres needs to write four kilobytes at a precise offset and know it landed. File storage hands you a directory tree over a network protocol like NFS, so several machines can share the same paths and the usual open-seek-write semantics still apply.

Object storage hands you neither. There's no device, no tree, and no filesystem underneath. What you get instead is a bucket holding a flat set of keys, addressed over an HTTP API: you PUT an object under a key, you GET it back, you DELETE it. The object is the whole unit. You don't open it, you don't seek inside it, and you don't hold a handle to it. What you get in exchange is a namespace that keeps growing without anyone tuning it, reachable from any machine that has credentials and a network route.

|                           | Block                       | File                             | Object                                 |
| ------------------------- | --------------------------- | -------------------------------- | -------------------------------------- |
| What you address          | Numbered blocks on a device | Paths in a directory tree        | Keys in a flat bucket                  |
| How you reach it          | Attached to one machine     | Network file protocol (NFS, SMB) | HTTP API from anywhere                 |
| Changing part of the data | Write blocks in place       | Seek and write in place          | Replace the whole object               |
| Concurrent writers        | One machine at a time       | Shared, with locking             | Last write wins, no locks              |
| Fits                      | Database files, VM disks    | Shared working directories       | Uploads, media, backups, static assets |

The same bytes, three access models. Object storage trades in-place editing and hierarchy for reach and scale.

The row that decides most architectures is the third one. Because an object is replaced rather than edited, object storage is a poor home for anything that mutates constantly in small pieces, which is why a database data directory belongs on a block device and never in a bucket. It's an excellent home for anything written once and read many times, which describes user uploads, images, video, build artifacts, exports, and backups.

## How a bucket really behaves

Most surprises with object storage come from carrying filesystem instincts into it. Four behaviours account for nearly all of them.

### The folders are a trick of the light

A key like `uploads/2026/photo.jpg` looks like a path, but it isn't one. It's a single flat string, and there is no directory object anywhere behind it. Clients fake the hierarchy at listing time: you ask for keys with a given prefix and pass `/` as a delimiter, and the API groups everything below the next slash into "common prefixes" that your file browser draws as folders. Nothing on the storage side knows those folders exist. So creating a deeply nested structure costs nothing, while "renaming a folder" means copying every object under the old prefix to a new key and deleting the originals, with no atomicity across the batch.

### Objects are written whole

The S3 API has no append and no partial write. You cannot open an object, jump to byte 900,000, and patch it. Changing one byte means uploading the entire object again under the same key. Multipart upload looks like an exception and isn't: it splits a large transfer into parts for reliability and parallelism, then assembles them into one object when you complete it. In the [S3 user guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) at the time of writing, a single object can reach 5 TB, a single PUT tops out at 5 GB, and multipart is the recommended route somewhere above 100 MB. Design accordingly: append-to-a-logfile patterns become write-a-new-object-per-batch patterns.

### Metadata rides along with the bytes

Every object carries HTTP headers you set at upload time: content type, cache control, content disposition, plus custom keys of your own. This is why a bucket can serve a website directly, and it's also the source of a classic first-day bug. Upload a PNG without a content type and the object gets a generic default, so the browser downloads it instead of rendering it. Whatever headers you set at upload are the headers the storage hands back when someone fetches that key, which makes them worth setting deliberately.

### Listing is an API call, not `ls`

`ListObjectsV2` returns keys in pages, capped at 1,000 per response by default on AWS as of writing, in lexicographic order, with a continuation token for the next page. Listing a prefix that holds a million keys is a thousand round trips, and there is no "sort by date" or "find files bigger than 10 MB" in the protocol. The practical rule follows from that: keep the index in your database and the bytes in the bucket. A page that renders a user's uploads should query Postgres for rows and fetch objects by key, never walk the bucket to find out what's in it.

## What "S3-compatible" actually means

Amazon launched S3 in 2006, and its HTTP interface became the shape everyone else built to. Today Ceph's object gateway, Cloudflare R2, Backblaze B2, Wasabi, and Runsite all speak some version of it, and so did MinIO before [its repository was archived](https://runsite.app/blog/minio-alternatives). "Compatible" refers to the wire protocol: REST over HTTP, buckets and keys, request signing with [Signature Version 4](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html), and the standard set of operations. In practice the operations that carry almost every application are a short list:

* `PutObject`, `GetObject`, `HeadObject`, `DeleteObject` — the whole read-write path for one key.
* `ListObjectsV2` — paginated listing with prefix and delimiter, the thing that draws your folders.
* `CreateMultipartUpload` and friends — large uploads split into parts, which the SDK usually handles for you.
* `CopyObject` — server-side copy, so moving data between keys doesn't travel through your app.
* Presigned URLs — a signed, time-limited link that lets a browser read or write one key without your credentials.

Because the protocol is the standard, the configuration surface for switching providers is tiny. An S3 client needs an endpoint, a region string, an access key, and a secret. Everything after that is the same code you already wrote:

python

```
import os
import boto3

# The endpoint is the only line that changes between providers.
s3 = boto3.client(
    "s3",
    endpoint_url=os.environ["S3_ENDPOINT"],
    aws_access_key_id=os.environ["S3_ACCESS_KEY"],
    aws_secret_access_key=os.environ["S3_SECRET_KEY"],
)

s3.upload_file("local.jpg", "my-bucket", "uploads/photo.jpg")
```

One detail catches people on day one: addressing style. AWS prefers virtual-hosted-style URLs, where the bucket is a subdomain (`my-bucket.endpoint/key`), and has been steering new buckets away from the older path-style form (`endpoint/my-bucket/key`). Plenty of compatible providers default to path-style, or support both. Every SDK has a switch for it — `addressing_style` in boto3's client config, `forcePathStyle` in the JavaScript SDK — and setting it wrong produces a DNS or 404 error that looks nothing like a configuration problem.

## Where compatibility frays

The core verbs above are safe to assume. Beyond them, S3 has grown twenty years of features, and a compatible provider implements the subset it chose to implement. This is the honest boundary of the compatibility claim, and it's where migrations go wrong. The features that vary most:

* **Versioning and object lock.** Keeping old versions of a key, and write-once retention for compliance, are common gaps.
* **Lifecycle rules.** Automatic expiry after N days, or transition to a colder tier, may be absent, partial, or configured somewhere other than the API.
* **Storage classes.** Infrequent-access and archive tiers are an AWS pricing construct as much as a technical one; many providers have a single class.
* **Bucket policies and ACLs.** Fine-grained, condition-based access rules are the least portable part of S3\. Most compatible stores offer simpler public/private buckets and scoped keys instead.
* **Event notifications.** Firing a webhook or queue message on upload is widely supported and widely different.
* **Server-side encryption options.** Encryption at rest is near-universal; customer-managed keys and KMS integration are not.
* **Replication and consistency.** S3 has offered [strong read-after-write consistency](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html#ConsistencyModel) since December 2020\. Compatible stores usually match it, but it's worth confirming rather than assuming, along with whatever cross-region replication they do or don't do.

None of that is a reason to distrust the compatibility claim. It's a reason to check it against your own workload rather than against the feature matrix in the abstract, and checking is an evening's work with a real bucket. Run these seven and you'll know:

1. Upload a file large enough to trigger your SDK's automatic multipart threshold, then download it and compare checksums.
2. Generate a presigned GET and a presigned PUT, confirm both work from outside your network, and confirm they stop working after the expiry.
3. List a prefix holding a few thousand keys. Check that pagination and delimiter grouping behave, and time it.
4. Set content type and cache control at upload, fetch the object over plain HTTP, and confirm the headers come back on the response.
5. Exercise the specific features you depend on: versioning, lifecycle expiry, a CORS preflight from your real browser origin.
6. Server-side copy an object, then delete several keys in one request, and check that the error codes your code branches on are the ones you get.
7. Point your existing integration test suite at the endpoint and let it run.

If all seven pass, portability is real for your workload: your application code is the same code, and the provider is a configuration value rather than an architectural commitment. If one fails, you've found it now, on a test bucket, instead of during a migration weekend.

## How do you choose an object storage provider?

With the model and the compatibility question settled, comparing providers comes down to a handful of axes. Most of them aren't on the pricing page, and the answers are usually a support ticket or a documentation search away.

| Axis                 | What to ask                                                                       | Why it decides things                                                                                                                                                                                                                                                                                                     |
| -------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Billing shape        | Which of storage, transfer, and requests are metered, and at what rate?           | Two providers with the same per-GB storage price can differ by orders of magnitude on the invoice if one meters egress. The arithmetic is in [S3 egress fees](https://runsite.app/blog/s3-egress-fees), and what the zero-egress providers charge instead is in [S3 alternatives without egress fees](https://runsite.app/blog/s3-alternatives-no-egress-fees). |
| Region and residency | Where do objects physically sit, and does anything replicate outside that region? | Decides your legal exposure and your latency. If you serve EU users, see [where to store EU user data](https://runsite.app/blog/gdpr-data-residency).                                                                                                                                                                                        |
| Durability           | What redundancy stands behind the number, and across how many failure domains?    | A durability figure without a described mechanism is marketing. The mechanism is the claim.                                                                                                                                                                                                                               |
| API surface          | Which features beyond the core verbs are implemented?                             | The checklist above, answered before migration rather than during it.                                                                                                                                                                                                                                                     |
| Public delivery      | Are public buckets CDN-backed, and can you control cache headers?                 | Determines whether serving assets straight from the bucket is viable or needs a CDN in front.                                                                                                                                                                                                                             |
| Access model         | Presigned URLs, key scoping, rotation without downtime, CORS configuration?       | This is how you let browsers touch storage without handing out credentials.                                                                                                                                                                                                                                               |
| Limits               | Maximum object size, multipart threshold, request rate caps?                      | Large media and high-throughput pipelines run into these first.                                                                                                                                                                                                                                                           |
| Exit                 | What does it cost, and how long does it take, to copy everything out?             | The cost of leaving is part of the cost of arriving.                                                                                                                                                                                                                                                                      |

The comparison that matters is rarely the one on the pricing page. These are the questions to put to any provider before you commit data to it.

Durability numbers are design targets, not promises

The famous "eleven nines" is [AWS's stated design target](https://aws.amazon.com/s3/) for S3, not a service-level guarantee, and other providers quote figures built on their own assumptions. Treat any durability number as an invitation to ask what redundancy sits behind it, and keep an independent copy of anything you couldn't recreate.

## Where object storage fits in your stack

The reason most teams arrive here at all is the container filesystem. On a modern platform, the disk your app writes to belongs to the container, and the container is replaced on every deploy. Files written at 10am are gone at 10:05 when you ship a fix. Run two instances and they don't even see each other's writes. Anything a user uploaded has to live somewhere that outlives a single container and is visible to all of them, which is the job a bucket exists to do.

The pattern that follows is a clean split. The row goes in the database: an id, the owner, the object key, the size, the content type, the timestamp, whatever your app needs to filter and sort on. The bytes go in the bucket under [a key you generate on the server](https://runsite.app/blog/upload-files-to-s3), ideally one that's stable and hard to guess. Your app queries Postgres to decide what exists and fetches from storage to serve it. The database is the index; the bucket is the payload.

Delivery splits the same way. Assets that are identical for everyone — images on a marketing page, a JavaScript bundle, a public download — live in a public bucket behind a CDN, cached at the edge and served without your app in the path. Files that belong to one user stay in a private bucket and get handed out as [short-lived presigned URLs](https://runsite.app/blog/s3-presigned-urls), so the browser talks to storage directly while your application keeps the credentials. Backups and archives are the third resident: write-once, read-rarely, exactly the shape object storage was built for, and the reason a scheduled job can [automate PostgreSQL backups to S3](https://runsite.app/blog/postgresql-backup-to-s3) into a bucket you control.

## How Runsite handles it

For a concrete reading on those axes, [S3-compatible object storage](https://runsite.app/services/s3-storage) on Runsite implements the standard API, so the AWS SDK, boto3, aws-cli, or any S3 client works with the endpoint and keys swapped and nothing else touched. Objects go up to 5 TB, with multipart recommended above 100 MB, and there's no cap on how much you store. Buckets are public or private; access keys carry read, write, delete, or admin scopes and rotate without downtime; presigned URLs and CORS configuration cover browser access. Public buckets are served through a global CDN with automatic cache headers, and the dashboard reports storage, transfer, and request counts.

On the billing axis, the shape is deliberately short: transfer isn't metered at all, in or out, so the only variable on the invoice is gigabytes stored, at €0.025 per GB per month after 5 GB free as of writing. That collapses the invoice to one predictable line, and the reasoning behind that pricing model is unpacked in [S3 egress fees](https://runsite.app/blog/s3-egress-fees). On the residency axis, objects sit in a German region, encrypted at rest with AES-256 and in transit with TLS, with no replication to non-EU regions and a signed GDPR Data Processing Agreement on every plan, which is the ground [where to store EU user data](https://runsite.app/blog/gdpr-data-residency) covers in full.

## The short version

Object storage is a different data model, not a cheaper disk: a flat set of keys, objects written whole, metadata carried with the bytes, and listing that costs an API call. "S3-compatible" is a real and useful guarantee about the wire protocol, which is what makes your application code portable, but it covers the core verbs rather than the long tail of versioning, lifecycle, policies, and events, so verify the features you actually depend on against a live bucket. After that, providers differ on billing shape, region, durability mechanism, API coverage, delivery, access model, limits, and the cost of leaving. If the model fits what you're building, the quickest way to check compatibility is to point your existing S3 client at a real bucket: [5 GB is free](https://runsite.app/services/s3-storage), which is enough to run every item on the checklist above.

Related service

## S3 Object Storage on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore S3 Object Storage](https://runsite.app/services/s3-storage)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is S3-compatible object storage?

It's object storage that implements Amazon S3's HTTP API: buckets and keys, AWS Signature Version 4 request signing, and the standard operations like PutObject, GetObject, ListObjectsV2, multipart upload, and presigned URLs. Because S3's interface became the de facto standard after its 2006 launch, providers such as MinIO, Ceph's object gateway, Cloudflare R2, Backblaze B2, Wasabi, and Runsite all speak it. In practice it means your existing AWS SDK, boto3, or aws-cli code runs against a different provider by changing the endpoint URL and the access keys, with no application changes.

### What is the difference between object storage and a file system?

A file system gives you a directory tree, file handles, and in-place editing: you can open a file, seek to an offset, and write a few bytes. Object storage gives you a flat set of keys reached over HTTP, where each object is written and replaced as a whole unit — there is no append, no partial write, and no real directory. Key names like uploads/2026/photo.jpg look like paths but are single flat strings; the folder view is produced by listing with a prefix and a delimiter. The trade is that you lose in-place editing and hierarchy, and gain a namespace that scales without tuning and is reachable from any machine with credentials.

### Can I use the AWS SDK with a non-AWS S3 provider?

Yes, and that's the main practical benefit of S3 compatibility. Set the client's endpoint URL to the provider's endpoint and supply that provider's access key and secret; the rest of your code, including multipart uploads and presigned URLs, stays the same. The one setting worth knowing about is addressing style: AWS prefers virtual-hosted-style URLs while many compatible providers default to path-style, so you may need to set the addressing style explicitly, via addressing\_style in boto3's client config or forcePathStyle in the JavaScript SDK. Beyond the core operations, features like versioning, lifecycle rules, and bucket policies vary by provider, so test the ones you rely on.

### How do I choose an S3-compatible storage provider?

Compare on eight axes rather than on the per-gigabyte price alone: which of storage, transfer, and requests are metered; where objects physically live and whether anything replicates outside that region; what redundancy stands behind the durability claim; which API features beyond the core verbs are implemented; whether public buckets are CDN-backed; the access model, meaning presigned URLs, key scoping, and CORS; hard limits such as maximum object size and multipart threshold; and what it costs to copy everything back out. Then validate the shortlist on a live test bucket with your own SDK before you migrate anything.

Keep reading

---

# Presigned URLs Explained: S3 Downloads and Uploads

> What an S3 presigned URL is and what it looks like, how long it lasts, and how a presigned POST uses the content-length-range policy to cap upload size.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Presigned URLs: Share and Upload Private S3 Files Without Handing Out Your Keys

StorageJuly 31, 202613 min read

# Presigned URLs: Share and Upload Private S3 Files Without Handing Out Your Keys

A presigned URL isn't a token the storage issued you. It's a signature you computed yourself, and almost everything people get wrong about expiry, revocation, and safety follows from that. Here's the mechanism, what one looks like, and how to use it for both downloads and uploads.

## The short version

A presigned URL is not a token the storage issued you. It is a request you signed with your own key, with the signature moved into the query string. Minting one is free and offline, it can never grant more than the key behind it, and revoking means rotating that key — which is why short expiries matter.

[Teo Marquardt](https://runsite.app/about#author)· Updated September 29, 2026

File uploads start out simple. The browser posts a form to your app, your app reads the bytes, your app writes them to a bucket. For a profile picture that arrangement will keep working forever. Then someone uploads a 400 MB video and the shape of the problem changes: the request pins a worker for two minutes, the bytes sit in memory or a temp file, your platform's request timeout starts to matter, and every byte crosses the network twice on its way to a place it could have gone directly.

The fix is to take your application out of the byte path and let the browser talk to storage itself. That raises the question that stops most people: the bucket is private, and the only credentials that can touch it live in your server's environment. A presigned URL is the standard way out, in both directions — handing one person a temporary link to download a private object, and letting a browser upload straight into a bucket. It's worth understanding literally rather than by analogy, because most of the confusion around expiry, revocation, and safety traces back to picturing it as a token the storage handed you. What it actually is: an HMAC your own code produced, using a secret it already had, over a request you described in advance. Everything else in this article follows from that one sentence. (If buckets, keys, and prefixes are new territory, [how S3-compatible object storage works](https://runsite.app/blog/s3-compatible-object-storage) sets up the model this article builds on.)

## What is a presigned URL?

A presigned URL is an ordinary request to your storage endpoint with the authentication moved out of the headers and into the query string. Normally an S3 client signs a request by computing an HMAC over a canonical description of it (method, path, selected headers, timestamp) using your secret key, then putting the result in an `Authorization` header. Presigning runs the same computation and writes the output into [the URL's query string](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html) instead:

* `X-Amz-Algorithm` — which signing scheme was used, in practice `AWS4-HMAC-SHA256`.
* `X-Amz-Credential` — the access key ID plus the date, region, and service it's scoped to.
* `X-Amz-Date` — when the signature was created.
* `X-Amz-Expires` — how many seconds it stays valid, counted from that date.
* `X-Amz-SignedHeaders` — which headers the signature covers, so they can't be swapped in flight.
* `X-Amz-Signature` — the HMAC itself.

### What a presigned URL looks like

Put together, those parameters produce a long but entirely readable string. Nothing in it is encrypted, and you can pick a presigned URL apart by eye:

text

```
https://s3.eu-central-1.example.com/uploads/user/42/9f3c1a.jpg
  ?X-Amz-Algorithm=AWS4-HMAC-SHA256
  &X-Amz-Credential=AKIAEXAMPLEKEY%2F20260819%2Feu-central-1%2Fs3%2Faws4_request
  &X-Amz-Date=20260819T091500Z
  &X-Amz-Expires=300
  &X-Amz-SignedHeaders=host
  &X-Amz-Signature=4a1f...c9b2
```

Strip the query string and what's left is the object's ordinary address, which is why a presigned URL for a private object and a plain link to a public one look almost identical. The access key ID is visible in `X-Amz-Credential`, and that's by design: the storage needs to know which secret to verify against. The secret itself never travels. Neither does anything about the person you sent it to, which is the property that makes the string worth protecting — the URL is the entire credential.

When the request arrives, the storage looks up the secret belonging to that access key ID, recomputes the signature over the same elements, and compares. A match inside the validity window is an authorized request; anything else is a 403.

Two consequences fall out of that, and both catch people off guard. Generating a presigned URL is pure local computation: your code runs an HMAC with a secret it already has, so there's no API call, no round trip, and no request to pay for. You could mint ten thousand of them in a loop while offline. The other is the mirror image of the first. The storage has no record of the URLs you issued, because it never saw them issued. There is no list to inspect and no entry to delete.

python

```
# No network call happens here. It's an HMAC over the request you described.
url = s3.generate_presigned_url(
    "put_object",
    Params={
        "Bucket": "uploads",
        "Key": "user/42/9f3c1a.jpg",   # you generate this, not the client
        "ContentType": "image/jpeg",
    },
    ExpiresIn=300,
)
```

## What the signature can and can't do

### The URL inherits the signing key's permissions, and nothing beyond them

A presigned URL doesn't create a permission. It delegates one the signing credentials already had. If the access key that signed it can only write to one bucket, that's the ceiling for every URL it produces, no matter how the URL is described to the person receiving it. So "is it safe to hand this out" is really the question "what could the key that signed it do," which has a cheap answer: sign uploads with a write-scoped key rather than an admin one, and the worst case for a leaked URL stays inside a boundary you drew on purpose.

### The clock is part of the signature

`X-Amz-Expires` is signed material, so nobody can extend a URL by editing it; changing the value invalidates the signature. It's also the only lifetime control the mechanism offers. On AWS at the time of writing, [a URL signed with long-lived credentials tops out at seven days](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) (604,800 seconds). Sign with temporary credentials instead, such as an assumed role, and the URL dies when that session does, even if the expiry you asked for was longer. Compatible providers may cap it lower or behave differently at the edges, which puts it on the list of things worth testing against a real bucket rather than assuming.

### There is no revoke button for a single URL

Since the storage keeps no record of what you minted, there's nothing to delete when you want one back. The lever that does exist is the credential underneath: rotate or remove the access key, and every URL ever signed with it stops verifying at once. That's a sledgehammer, and it's the strongest practical argument for short expiries. A URL that lives five minutes needs no revocation story, because it revokes itself before anyone finishes writing the incident ticket.

## Sharing a private file: the presigned GET

The download case is the older half of this, and the simpler one. You have an object nobody should reach without permission, say an invoice or a paid asset, and you want one person to have it for the next few minutes. Sign a `GET` for that key, hand over the URL, and they fetch it straight from storage. Your application never streams the bytes, which is the point: a 200 MB export served through your app is a worker held open for the length of a download you don't control.

bash

```
# One line, no SDK. Prints a URL valid for 300 seconds.
aws s3 presign s3://uploads/user/42/invoice-2026-07.pdf --expires-in 300
```

Two details separate a download link that works from one that annoys people. The first is the filename. Objects are usually keyed by something unguessable, so the browser saves `9f3c1a.pdf` unless you say otherwise; signing `response-content-disposition` lets you override what the download is called without renaming anything in the bucket. The second is caching, covered in the risk table below — a presigned GET carries no identity, so a shared cache that keys on path alone can serve one person's object to the next request that comes along.

javascript

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

const url = await getSignedUrl(
  client,
  new GetObjectCommand({
    Bucket: "uploads",
    Key: "user/42/invoice-2026-07.pdf",
    ResponseContentDisposition: 'attachment; filename="invoice-july.pdf"',
  }),
  { expiresIn: 300 }
);
```

One thing a presigned GET is not is a way to publish. Signing a fresh URL for every visitor who loads an image works, and then it stops working the moment a CDN sits in front of it, because each visitor gets a different URL and the cache hit rate collapses. Assets everyone is allowed to see belong in a public bucket. Presigning is for objects where the answer to "may this person have it" is decided per request.

### Signed URL or presigned URL?

The two terms get used interchangeably and mostly deserve to be, but they point at different machinery when a CDN is involved. A presigned URL is an S3 concept: it's signed with a storage access key and verified by the storage, so it works against any S3-compatible endpoint, Cloudflare R2 and [self-hosted servers like Garage](https://runsite.app/blog/minio-alternatives) included, using the same SDK call. A CloudFront signed URL is signed with a separate key pair and verified at the edge before the request ever reaches a bucket, which is what lets it carry conditions like an IP restriction. Google Cloud Storage calls its equivalent a signed URL, and it's the same idea as S3 presigning under a different name. If someone hands you code using `generate_presigned_url` or `getSignedUrl` against a bucket endpoint, it's the mechanism described here.

## Three ways to get a file into a bucket

With the mechanism clear, the design choice comes into focus. There are three arrangements in common use, and they differ mostly in how much you can force on the client.

| What you're comparing            | Proxy through your app                                                                                                                            | Presigned PUT                                                           | Presigned POST with policy                                              |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Where the bytes travel           | Browser → your app → bucket                                                                                                                       | Browser → bucket                                                        | Browser → bucket                                                        |
| Who holds credentials            | Your server, exclusively                                                                                                                          | Your server signs, the browser gets a URL                               | Your server signs, the browser gets fields and a policy                 |
| What you can force on the client | Everything: you're holding the file                                                                                                               | Key, method, expiry, and any header you signed                          | All of that, plus a size range, a key prefix, and a content-type prefix |
| Practical size ceiling           | Your request timeout and memory                                                                                                                   | Single-PUT limit, 5 GB on AWS as of writing                             | Same, with a range the storage enforces                                 |
| What it costs you                | Worker time, memory, bandwidth on both legs                                                                                                       | One HMAC                                                                | One HMAC                                                                |
| Reach for it when                | Files are small and you must inspect them, or the uploader is a server-side job like a [scheduled database backup](https://runsite.app/blog/postgresql-backup-to-s3) | The client is trusted, the key is yours, and size is checked afterwards | Browsers on the public internet are uploading arbitrary files           |

The same upload, three architectures. The column that usually decides it is what you can constrain, not where the bytes travel.

The proxy row is worth pricing before you dismiss it. Every byte crosses your application on the way in, occupying a worker and a chunk of memory for the whole transfer, and it crosses again on the way out whenever your app serves the file back instead of pointing at storage. On a metered provider that return leg is billable transfer, which is where [S3 egress fees](https://runsite.app/blog/s3-egress-fees) come from. Proxying is still the right answer when you genuinely need to look at the bytes before they land, such as scanning or transcoding on receipt. It's the wrong answer as a default.

The difference between the two presigned forms is the part tutorials tend to skip, and it's the whole decision. A presigned PUT signs one specific request: this method, this key, these headers, until this moment. What it doesn't do is constrain the body. A client handed a PUT URL for a 200 KB avatar can send three gigabytes instead, and it will be accepted, because size was never part of what you signed. Past the single-PUT ceiling the arrangement changes again, since the file has to travel in parts; [uploading files to S3 in practice](https://runsite.app/blog/upload-files-to-s3) covers the multipart flow and what the browser ends up orchestrating.

### S3 presigned POST: the content-length-range policy condition

Presigned POST is a different operation with a different shape. Rather than signing a URL you sign a short [policy document](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html) describing what an acceptable upload looks like, then hand the browser that policy, its signature, and a set of form fields. The storage checks the upload against the policy before accepting it:

json

```
{
  "expiration": "2026-07-31T12:05:00Z",
  "conditions": [
    {"bucket": "uploads"},
    ["starts-with", "$key", "user/42/"],
    ["starts-with", "$Content-Type", "image/"],
    ["content-length-range", 1024, 5242880]
  ]
}
```

`content-length-range` is the line that earns the extra complexity. It turns a size limit into something the storage rejects at the door, rather than something you discover afterwards from a bill or a `HeadObject`. The prefix conditions do the same for key placement. Note that POST-with-policy is one of the S3 operations whose support varies across compatible providers, so confirm it works the way you expect on your provider before you design around it.

The two numbers are a minimum and a maximum in bytes, so the policy above accepts files between 1 KB and 5 MB. Anything outside that range never becomes an object. S3 answers the form post with a 400 and one of two error codes, `EntityTooLarge` ("Your proposed upload exceeds the maximum allowed object size.") or `EntityTooSmall` ("Your proposed upload is smaller than the minimum allowed object size."), and the browser gets that XML body back instead of a redirect or a 204\. Parse it in the client. If you don't, a user who picked a 12 MB photo sees a raw XML error and no idea what went wrong.

### Can a presigned PUT enforce a size limit?

Only an exact one. If you pass the file's size when you sign and your SDK includes `Content-Length` in the signed headers, a request with any other length fails the signature check. So you get a fixed size, not a ceiling: the client has to tell your server the exact byte count first, and nothing stops it from declaring 3 GB. A range isn't something PUT can express. If you need "no larger than 5 MB" enforced by the storage rather than by your own code, use POST with `content-length-range`, or accept the PUT and check the object's size with `HeadObject` afterwards, deleting anything over the limit.

## Are presigned URLs secure?

In the narrow sense the question usually means, yes: the mechanism exists precisely so a browser can touch storage without ever seeing your credentials, and the signature can't be modified without breaking. The failures happen one level up, in what the signature was never asked to cover.

A presigned URL is a bearer credential

Whoever holds the string can use it, exactly as the signature permits, until it expires. There's no identity check layered on top and no second factor. Treat one the way you'd treat a password reset link: fine to send to the person who asked for it, unwise to leave lying around in a log.

That framing turns most of the risk list into something concrete. Six patterns account for nearly every presigned-URL incident worth having an opinion about:

| What bites                            | Why it happens                                                                                                                                                                                                                                                                                                                                  | What to do instead                                                                                                                                    |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| An expiry measured in days            | The window in which a leaked link still works is exactly the expiry you chose, and "a week, to be safe" is a common default.                                                                                                                                                                                                                    | Minutes for uploads, minutes to an hour for downloads. Regenerating on demand costs an HMAC, so there's no reason to be generous.                     |
| The client chooses the object key     | A key that arrives from the browser can point anywhere in the bucket, including over another user's object or outside your prefix.                                                                                                                                                                                                              | Generate the key server-side from the authenticated session, with a random component so it isn't guessable.                                           |
| Trusting the declared content type    | The client states Content-Type, the storage records it and hands it back on download. It's a label the uploader wrote, not a verification.                                                                                                                                                                                                      | Check the bytes yourself after upload, and serve user content from a separate domain with an explicit Content-Disposition.                            |
| No ceiling on upload size             | A plain presigned PUT accepts whatever the client sends, up to the protocol limit.                                                                                                                                                                                                                                                              | Use POST with content-length-range where it's supported. Otherwise HeadObject after the upload and delete anything oversized.                         |
| The URL ends up in logs               | Query strings land in access logs, error-tracker breadcrumbs, analytics, and support screenshots, and the signature travels with them. Browsers help a little (the default strict-origin-when-cross-origin policy keeps the query string out of the Referer header on cross-origin navigation), but nothing protects you from your own logging. | Redact query parameters in logging middleware and in your error tracker. Short expiries cap the exposure that gets through anyway.                    |
| A presigned GET behind a shared cache | A cache keyed on path rather than the full URL can hand one user's private object to the next request that asks for it.                                                                                                                                                                                                                         | Send Cache-Control: no-store on private objects, or keep them out of shared caches. Assets meant for everyone belong in a public bucket behind a CDN. |

What the signature doesn't cover, and what to do about each. All six are cheap to fix at design time and awkward to fix later.

One thing that belongs above the table rather than in it: your application still has to decide whether this user may upload here, before it signs anything. The signature proves the request carries your authority. It says nothing about who asked for it, and no amount of tightening the expiry compensates for an endpoint that signs a URL for anyone who calls it.

## The upload your server never saw

Moving your application out of the byte path removes a guarantee you were relying on without noticing. Previously your app knew the upload had happened, because it did the uploading. Now the browser talks to storage and your server finds out only if you arrange for it to. The trade is usually worth it: it's also the cleanest way out of a [413 Request Entity Too Large](https://runsite.app/blog/413-request-entity-too-large) on big uploads, because the file never meets the proxy and framework limits that produce one.

The version that looks obvious is to insert the database row at the moment you sign the URL. That row is a claim about a file that may never arrive: the user closes the tab, the connection drops on a train, the upload fails at 80%. Your listing page now renders entries whose objects don't exist, and every one of them is a 404 in front of a real user. The row has to lag the bytes, not lead them.

1. The client asks to upload. You authorize the request, generate the object key yourself, and write a row in a `pending` state. The row is a reservation, not a fact.
2. You sign a short-lived URL for that exact key and return it.
3. The browser uploads straight to storage, then calls your API back to report that it finished.
4. On that callback you run `HeadObject` against the key. It returns size, content type, `ETag`, and last-modified without transferring the bytes, so you can compare what landed against what you signed for.
5. If the metadata checks out, the row moves to `ready`. That transition is the moment the file exists as far as your application is concerned.

The `HeadObject` is the load-bearing step. It's what converts a claim made by the client into something your server verified, at the cost of one request against a key you already know. Skip it and the callback becomes an unauthenticated "trust me, it worked" from the least trustworthy participant in the flow.

Two loose ends survive the callback, and a periodic sweep handles both. Rows still `pending` well past any plausible upload time were abandoned, so delete them along with anything sitting under their key. Objects with no row pointing at them are orphans, usually uploads that completed after you gave up waiting, and they quietly accumulate storage until something removes them. Where a provider supports event notifications, the storage can tell your application about a completed upload directly and the client's callback stops being the only signal, though that support varies between S3-compatible stores enough to belong in the [compatibility checks you run against a live bucket](https://runsite.app/blog/s3-compatible-object-storage).

## Why it works in curl and fails in the browser

A presigned URL that uploads happily from a terminal and dies in the page is almost always [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html). `PUT` is not a simple method, so before the real request the browser sends an `OPTIONS` preflight. The presigned query string is still sitting on that request line, but the storage never checks a signature on an `OPTIONS`: it answers from the bucket's CORS configuration alone, as if your signature weren't there. Which means the bucket has to allow your origin, the methods you use, and the headers your client sets. If your code reads `ETag` off the response, that header also has to be listed as exposed, or the browser hides it from JavaScript even though it arrived. The console reports a generic CORS failure with no useful detail, which is why this one costs an afternoon the first time and five minutes forever after.

## How Runsite handles it

On Runsite's [S3-compatible object storage](https://runsite.app/services/s3-storage), presigned URLs cover both jobs from this article: sharing one private file, and letting a browser upload straight into a bucket. The API is the standard one, so `generate_presigned_url` in boto3, `getSignedUrl` in the JavaScript SDK, and `aws s3 presign` all work once the endpoint and keys point at Runsite, with nothing else in your code touched.

The revocation lever from earlier is the access key, and keys here are built for that use: each one carries a read, write, delete, or admin scope and rotates without downtime. Sign uploads with a write-scoped key and rotating it invalidates every outstanding URL it produced without disturbing the keys your other services use. CORS is configurable per bucket, buckets are private or public, and public ones are served through a global CDN with automatic cache headers, which keeps the line between "signed for one person" and "cached for everyone" where it belongs.

Transfer isn't metered in either direction, so receiving a direct browser upload costs nothing and serving a presigned download costs nothing. Gigabytes stored is the only variable left on the invoice, at €0.025 per GB per month after 5 GB free as of writing. Objects go up to 5 TB with multipart recommended above 100 MB, everything is encrypted at rest with AES-256 and in transit with TLS, and objects sit in the German region under a signed GDPR Data Processing Agreement on every plan.

## The short version

A presigned URL is a request you signed with your own key, with the signature moved into the query string. Minting one is free and offline; it can never grant more than the key behind it; and taking one back means rotating that key, which is why short expiries do most of the security work. Sign a `GET` to hand one person a private object for the next few minutes, and a `PUT` or `POST` to let a browser write into the bucket without your app touching the bytes. Choose the upload form by how much you need to constrain the uploader: a presigned PUT pins the key, the method, and the deadline while leaving the body wide open, and a presigned POST adds a policy the storage enforces on size, prefix, and content type. Either way the signature only proves the request carries your authority, so authorize the user before you sign, generate keys server-side, keep the URL out of your logs, and confirm the object with a `HeadObject` before your application treats it as real. To try the whole flow end to end, [5 GB is free](https://runsite.app/services/s3-storage), which is enough to sign a URL, upload straight from a browser, and watch the key appear.

Related service

## S3 Object Storage on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore S3 Object Storage](https://runsite.app/services/s3-storage)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is a presigned URL?

A presigned URL is a normal request to your object storage with the authentication moved from the headers into the query string. Your application computes an AWS Signature Version 4 HMAC over the request using its own secret key, and the result travels in parameters like X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders, and X-Amz-Signature. When someone uses the URL, the storage recomputes the signature with the same secret and allows the request if it matches and hasn't expired. Because the signature is computed locally, generating a presigned URL involves no API call and costs nothing, and the storage keeps no record that the URL exists.

### Are presigned URLs secure?

They're secure in the sense that they never expose your credentials and can't be modified without invalidating the signature. What they are is a bearer credential: anyone holding the string can use it, exactly as signed, until it expires. The practical risks live around the edges rather than in the cryptography. Keep expiries in minutes rather than days, generate the object key on the server so a client can't overwrite someone else's file, treat any client-declared content type as unverified, cap upload size with a presigned POST policy where it's supported, redact query strings in your logs and error tracker, and keep presigned downloads out of shared caches. Also authorize the user before signing, since the signature proves your application approved the request but says nothing about who asked.

### How long does a presigned URL last, and can I revoke one?

The lifetime is set by X-Amz-Expires when you sign, and it's part of the signed material, so it can't be extended afterwards. On AWS at the time of writing, a URL signed with long-lived credentials can last at most seven days (604,800 seconds); if you sign with temporary credentials, it stops working when that session expires regardless of the value you asked for, and compatible providers may impose their own limits. There's no way to revoke a single URL, because the storage never recorded it being issued. The only lever is the credential that signed it: rotating or deleting that access key invalidates every URL produced with it at once. That bluntness is the main reason to keep expiries short.

### What does a presigned URL look like?

It looks like the object's ordinary address with a long query string bolted on. The path is the endpoint, bucket, and key; the query string carries X-Amz-Algorithm (in practice AWS4-HMAC-SHA256), X-Amz-Credential holding the access key ID plus the date, region and service it is scoped to, X-Amz-Date, X-Amz-Expires in seconds, X-Amz-SignedHeaders listing which headers the signature covers, and X-Amz-Signature itself. None of it is encrypted, so you can read a presigned URL by eye and see when it was signed and how long it lasts. The secret key never appears in the string.

### How do I create a presigned URL?

With whichever S3 SDK you already use, or the CLI. In Python and boto3 it is s3.generate\_presigned\_url("get\_object" or "put\_object", Params={...}, ExpiresIn=300). In JavaScript it is getSignedUrl from @aws-sdk/s3-request-presigner, passing a GetObjectCommand or PutObjectCommand. From a terminal, aws s3 presign s3://bucket/key --expires-in 300 prints one. All of them run locally. Signing is an HMAC over the request description using credentials the process already holds, so nothing leaves the machine and nothing lands on a bill. The same calls work against any S3-compatible endpoint once the endpoint URL and keys point at it.

### Can anyone access a presigned URL?

Anyone holding the full string can, which is the whole reason to be careful with it. A presigned URL is a bearer credential: the storage checks the signature and the expiry, not who is presenting it. There is no login step, no second factor, and no way to bind it to one person. That makes short expiries the main control you have, along with keeping the URL out of access logs, error trackers, and shared caches, since the query string travels wherever the URL does.

### What is the difference between a signed URL and a presigned URL?

Usually nothing, but the terms point at different machinery when a CDN is in the picture. A presigned URL is the S3 concept: signed with a storage access key, verified by the storage, and portable across S3-compatible endpoints such as MinIO and Cloudflare R2\. A CloudFront signed URL is signed with a separate key pair and validated at the edge before the request reaches a bucket, which is what lets it carry extra conditions like an IP restriction. Google Cloud Storage calls its own equivalent of S3 presigning a signed URL. Code calling generate\_presigned\_url or getSignedUrl against a bucket endpoint is doing S3 presigning regardless of which word the docs used.

### What is the difference between a presigned PUT and a presigned POST?

A presigned PUT signs one specific request, fixing the object key, the HTTP method, any headers you included, and the expiry. It places no limit on the request body, so a client given a PUT URL for a small image can upload a multi-gigabyte file instead. A presigned POST signs a policy document rather than a URL, and the storage validates the upload against that policy before accepting it. The policy can require conditions such as content-length-range for a minimum and maximum size, and starts-with rules on the key prefix and content type. Use PUT when the uploader is trusted and you generate the key yourself; use POST when a browser on the public internet is uploading and you want the storage to enforce the limits. POST policy support varies between S3-compatible providers, so verify it on a live bucket first.

### How do I limit upload size with an S3 presigned POST?

Add a content-length-range condition to the POST policy. It takes a minimum and a maximum in bytes, so \["content-length-range", 1024, 5242880\] accepts files from 1 KB to 5 MB. The storage checks the upload against the signed policy before it becomes an object, and rejects anything outside the range with a 400 and the error code EntityTooLarge or EntityTooSmall, so an oversized file never lands in the bucket. A presigned PUT cannot do this: it can only pin one exact Content-Length, not a range. Support for POST policies varies between S3-compatible providers, so check that yours accepts the condition before relying on it.

Keep reading

---

# Uploading Files to S3: Keys, Multipart, and Retries

> How S3 uploads behave in production: object keys you can't change later, when multipart is required, what's safe to retry, and why ETag won't verify a big file.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Uploading User Files to S3: Keys, Multipart, and What to Retry

StorageAugust 3, 202611 min read

# Uploading User Files to S3: Keys, Multipart, and What to Retry

Uploading a file is three lines of SDK code, and they stay correct until a real user shows up with a 3 GB video on hotel wifi. Here's what comes after the tutorial: naming objects you can't rename, splitting large files, and knowing which failures are safe to repeat.

## The short version

Generate the object key on the server and keep it opaque, because renaming an object means copying it. Use a single PUT for small predictable files and multipart above roughly 100 MB, where part size times 10,000 caps the file. Abort incomplete uploads, and confirm with HeadObject before your application treats an upload as real.

[Teo Marquardt](https://runsite.app/about#author)

Uploading a file to object storage is three lines in any S3 SDK, and those three lines are correct. They keep being correct until a real user turns up: a 3 GB video over hotel wifi, a file called `Screenshot 2026-07-14 (1).png`, a laptop lid closing at 80%. What breaks then has nothing to do with the `PutObject` call, which is why the tutorial that taught you the call is no help.

This is about the part that comes after. How to name an object you'll never be able to rename, when a single request stops being enough, which failures you can repeat and which you can't, and why a storage bill can exceed the sum of everything visible in the bucket. It assumes the writing side is already settled: if you're still choosing between proxying uploads through your application and letting the browser write to storage directly, [presigned URLs](https://runsite.app/blog/s3-presigned-urls) covers that decision and the security model behind it. For buckets, keys, and prefixes as concepts, [how S3-compatible object storage works](https://runsite.app/blog/s3-compatible-object-storage) sets up the vocabulary used here.

## The object key is the one decision you can't take back

In a filesystem you rename things. Object storage has no rename. A key is fixed when the object is written, and changing it means copying the object to the new key and deleting the old one, which reads and rewrites every byte. That falls out of a property the pillar article covers: objects are written whole, never edited in place. A server-side copy handles up to 5 GB in a single call on AWS as of writing, and larger objects have to be copied in parts.

With four objects in a bucket this is a footnote. With four million it's a migration, and the key is usually your URL as well, so anything already handed out breaks when you move it. Ten minutes of thought before the first upload is cheaper than any amount of thought after the hundred-thousandth.

### Don't let the client name the object

Accepting the browser's filename as the key is the default in most tutorials and it fails in ordinary use, before anyone tries to attack you. Two users upload `avatar.jpg` and one overwrites the other. Keys are case-sensitive, so `Photo.JPG` and `photo.jpg` are two separate objects, which surprises everyone whose laptop says otherwise. Spaces and parentheses need escaping every time the key becomes a URL, and they will. [Keys cap out at 1024 bytes of UTF-8](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html), so a long name behind a long prefix can hit a ceiling nobody thought about. The security side of the same decision, including what a client-chosen key can reach in your bucket, is in the [presigned URL risk table](https://runsite.app/blog/s3-presigned-urls).

### What a durable key looks like

* **An opaque identifier from the server.** A UUID or ULID, generated where the request is authorized. Random means unguessable, which matters because a key is half of a URL.
* **A prefix carrying whatever you'll need to sweep by.** Listing works on prefixes and nothing else, so `user/42/2026/07/` lets you find one tenant's month and `uploads/` lets you find nothing in particular.
* **The extension, kept as a hint.** Some clients infer a content type from it, so it's useful. It's a claim by the uploader, not a fact about the bytes.
* **The display name stored somewhere else.** Object metadata is specified as US-ASCII, so a Cyrillic or emoji filename needs encoding to survive there. The database row that tracks the file is the better home for it, and it's the row you'll join against anyway.

python

```
import datetime
import pathlib
import uuid

def upload_key(user_id: str, original_name: str) -> str:
    extension = pathlib.Path(original_name).suffix.lower()[:10]
    day = datetime.date.today().isoformat()
    return f"user/{user_id}/{day}/{uuid.uuid4().hex}{extension}"

s3.put_object(
    Bucket="uploads",
    Key=upload_key("42", "Screenshot 2026-07-14 (1).png"),
    Body=data,
    ContentType="image/png",
)
```

Serving the file back with its original name is then a `Content-Disposition` header at download time, set from the database row. The key stays boring forever.

## One PUT, or many?

A single `PutObject` sends the whole body in one request, capped at 5 GB on AWS as of writing. The cap is rarely what stops you. What stops you is the failure mode: one dropped connection at 90% and the transfer restarts from zero, because a half-finished PUT leaves nothing behind to resume from. And if uploads still pass through your own server first, the wall you hit is usually far below 5 GB: a proxy's body limit answering with [413 Request Entity Too Large](https://runsite.app/blog/413-request-entity-too-large).

| What you're comparing        | Single PUT                                                      | Multipart upload                                   |
| ---------------------------- | --------------------------------------------------------------- | -------------------------------------------------- |
| Largest object               | 5 GB on AWS                                                     | Up to 5 TB, subject to the part arithmetic below   |
| Cost of a dropped connection | The whole file, every time                                      | One part                                           |
| Parallelism                  | One request on one connection                                   | Parts upload concurrently and out of order         |
| Resume after a failure       | No such thing, start again                                      | Re-send the parts that didn't land                 |
| API calls per file           | One                                                             | Two, plus one per part                             |
| What your code owns          | Nothing                                                         | Part sizing, tracking, completion, cleanup         |
| Reach for it when            | Files are small and predictable: avatars, documents, thumbnails | Files run past \~100 MB, or the network is a phone |

The row that decides it is usually the second one, not the first.

Multipart usually gets explained as a speed feature. The parallelism is real, but throughput isn't what should decide it. A 2 GB upload over a mobile connection has a serious chance of dying somewhere in the middle, and the gap between losing 16 MB and losing 2 GB is the gap between a retry the user never notices and a feature they stop using.

## How multipart actually works

A multipart upload is a single object sent as a set of independently uploaded parts, assembled by the storage into one object at the end. It's three operations, shaped like a transaction:

1. `CreateMultipartUpload` returns an upload ID. Nothing is stored at the key yet. The ID is the handle for everything that follows.
2. `UploadPart` sends one chunk, tagged with that upload ID and a part number. Parts can go in any order, over as many connections as you like, and each response carries an `ETag` you have to keep.
3. `CompleteMultipartUpload` sends the list of part numbers and their ETags. The storage assembles them. Until this call succeeds, no object exists at the key.

The limits are worth memorising because they constrain your design. As of writing, [part numbers run from 1 to 10,000](https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html), every part except the last must be at least 5 MiB, and no part may exceed 5 GiB. One line of arithmetic follows: **part size × 10,000 is the largest file that part size can carry.** At the 5 MiB minimum you top out around 50 GB. Reaching the 5 TB object ceiling needs parts of at least 500 MiB.

Which makes part size a real decision rather than a constant to copy off a blog. Small parts mean cheap retries and more requests; large parts mean fewer requests and more to re-send when one fails. Somewhere between 8 and 64 MiB suits most application uploads, and the rule that matters more than the exact number is that ten thousand of your parts have to clear your largest plausible file.

python

```
from boto3.s3.transfer import TransferConfig

config = TransferConfig(
    multipart_threshold=64 * 1024 * 1024,  # one PutObject below this
    multipart_chunksize=16 * 1024 * 1024,  # part size above it
    max_concurrency=8,                     # parts in flight
)

# Chooses the strategy, splits the file, retries failed parts,
# and aborts the upload if it gives up.
s3.upload_file("render.mp4", "uploads", key, Config=config)
```

## The parts you forgot about are still on the bill

`CompleteMultipartUpload` is what turns parts into an object. Nothing turns them back. An upload that stops halfway, because the process was killed or the client crashed or someone shipped a deploy, leaves its parts in the bucket under an upload ID nobody owns any more. They don't show up in a listing of objects, since there is no object. They do occupy storage, and AWS bills them as storage until the upload is aborted.

A bucket can be billed for more than you can see in it

Abandoned multipart parts are invisible to a normal listing and visible on the invoice. If your storage line looks larger than the objects justify, that gap is the first place to look.

There are two ways to deal with it. `ListMultipartUploads` shows what's in flight and `AbortMultipartUpload` discards one, which is enough when you're cleaning up after a known incident. For the ongoing case, [a lifecycle rule with AbortIncompleteMultipartUpload](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpu-abort-incomplete-mpu-lifecycle-config.html) sweeps anything older than N days without you thinking about it. Lifecycle configuration is one of the areas where S3-compatible providers diverge, so it belongs in the [compatibility checks you run against a live bucket](https://runsite.app/blog/s3-compatible-object-storage) rather than in your assumptions.

Worth keeping separate from transfer costs: zero-egress storage means moving bytes isn't metered, not that occupying space is free. Where transfer is metered, the arithmetic is its own problem and [S3 egress fees](https://runsite.app/blog/s3-egress-fees) works through it.

## Where the SDK's help stops

The `upload_file` call above is doing more than it looks. It picks single-PUT or multipart based on the threshold, splits the file, runs parts concurrently, retries the ones that fail, and aborts the upload if it eventually gives up. boto3 calls this [a managed transfer](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-uploading-files.html); the JavaScript SDK has the same thing in `Upload` from `@aws-sdk/lib-storage`. It's the right answer whenever your own code holds the credentials, which covers a server accepting a proxied upload, a worker moving a render into storage, or [a scheduled job pushing a database dump](https://runsite.app/blog/postgresql-backup-to-s3).

The constraint hides in that clause. A managed transfer constructs and signs every request itself, so it needs the secret key. Browser code never has one, which is the whole premise of [direct uploads with presigned URLs](https://runsite.app/blog/s3-presigned-urls). The helper is unavailable exactly where a large file upload hurts most, and nobody mentions this until you're halfway through the feature.

Multipart straight from a browser therefore looks different. Your server does the signing, one part at a time, and the client runs the state machine:

1. The client asks to start an upload. Your server authorizes it, generates the key, calls `CreateMultipartUpload`, and returns the upload ID.
2. For each part the client asks for a signed URL, or collects a batch up front. The server signs `upload_part` for that key, upload ID, and part number.
3. The client PUTs each part to its own URL and keeps the `ETag` from the response, retrying individual parts on failure.
4. The client sends back the part numbers and ETags it collected, and your server calls `CompleteMultipartUpload`.

python

```
# Your server signs one part at a time. The browser never sees a key.
part_url = s3.generate_presigned_url(
    "upload_part",
    Params={
        "Bucket": "uploads",
        "Key": key,
        "UploadId": upload_id,
        "PartNumber": part_number,
    },
    ExpiresIn=900,
)
```

That's four endpoints and a client-side loop where the server-side version was one function call. Reading the `ETag` off each part response also means the bucket's CORS configuration has to list it as an exposed header, or the browser hides it from your JavaScript even though it arrived. Keeping bytes out of your application costs you this loop, which is worth knowing before you promise someone resumable uploads.

## What's safe to retry

Uploads fail in ways that are mostly retryable. The useful question is which operations you can repeat without reasoning about them first.

| Operation               | Repeating it is  | What that means in practice                                                                                                                                                               |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| PutObject               | Safe             | The key is fixed and the body is identical, so a retry overwrites the object with the same bytes. The cost is a wasted transfer.                                                          |
| UploadPart              | Safe             | A part number can be re-sent and the last successful write for that number wins. Keep the ETag from the response you actually acted on.                                                   |
| CompleteMultipartUpload | Safe, but verify | A completion can succeed while its response is lost in transit. HeadObject on the key settles whether the object is there before you retry blindly.                                       |
| AbortMultipartUpload    | Safe             | Aborting an upload that's already gone returns a not-found you can ignore.                                                                                                                |
| Anything answering 4xx  | Pointless as-is  | The request was rejected on its merits and will be again. The exception worth special-casing is an expired signature, where the fix is a fresh presigned URL rather than another attempt. |

Retry behaviour of the operations an upload path actually uses.

For 5xx responses and throttling, exponential backoff with jitter is standard and every AWS SDK does it by default up to a configurable attempt count. What the SDK won't decide is how many times your application should try before it tells the user something went wrong.

### The ETag is not a checksum for multipart objects

Retries protect you against transfers that fail loudly. They say nothing about a transfer that succeeded with the wrong bytes, and the usual homegrown answer to that is quietly broken.

For an object uploaded in a single PUT without SSE-C or SSE-KMS encryption, the ETag happens to be the MD5 of the body, and plenty of verification code depends on that coincidence. It stops holding the moment the object arrives in parts. A multipart object's ETag is a hash over the concatenated part hashes, followed by a dash and the part count, so `-14` at the end tells you the file came in fourteen pieces and nothing at all about its contents. It also shifts with the part size used, meaning the same file uploaded with different settings produces different ETags.

An ETag ending in -N is not an MD5

It's a hash of hashes plus a part count, and it depends on how the file was split. Comparing it against the MD5 of a local file will fail on every multipart upload, correct or not.

Integrity you can act on has to be requested. `Content-MD5` on a single PUT makes the storage reject a body that doesn't match what you declared, and the [additional checksum algorithms](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html) (CRC32, CRC32C, SHA-1, SHA-256 as of writing) work per part as well as per object. Support for the newer ones varies across S3-compatible providers. The approach that works everywhere is computing your own checksum and storing it in the row that tracks the file.

## Progress, cancel, and resume in the browser

A progress bar is the thing users notice missing. `fetch()` reports progress on the response, not the request: there's no upload progress event as of writing, and streaming request bodies with `duplex: "half"` are supported unevenly enough that you can't rely on them. So a progress bar in a browser still means `XMLHttpRequest` and its [upload.onprogress](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestUpload). Multipart hands you a second, coarser signal at no cost, since parts completed over parts total is already a percentage.

javascript

```
const xhr = new XMLHttpRequest();

xhr.upload.onprogress = (event) => {
  if (event.lengthComputable) setProgress(event.loaded / event.total);
};

xhr.open("PUT", presignedUrl);
xhr.setRequestHeader("Content-Type", file.type);
xhr.send(file);
```

Cancelling is `xhr.abort()` or an `AbortController`, and it stops the request without cleaning up after it. A cancelled multipart upload still needs an `AbortMultipartUpload` from your server, and a cancelled single PUT may well have landed anyway.

Resuming an interrupted upload is possible only with multipart, and only if the client kept its state: the upload ID, the part numbers that succeeded, and their ETags. Persist that in `localStorage` or IndexedDB and a page reload can pick up where the connection died, as long as the upload ID hasn't been swept by your lifecycle rule in the meantime. A single PUT has no resume story to offer. It starts from zero, every time, forever.

## After the bytes land

Once the browser writes to storage directly, your application is out of the byte path and learns about the file only if you arrange for it. The shape of that arrangement is a database row in a `pending` state, a callback from the client, and a `HeadObject` that checks what actually arrived against what you signed for before the row becomes `ready`. It's [worked through step by step in the presigned URL article](https://runsite.app/blog/s3-presigned-urls), so it isn't repeated here.

Two jobs stay yours regardless of who did the uploading. The content type on the object was declared by the uploader, so when the distinction matters, read the first bytes and check them against the extension instead of trusting either. And serving files back is a separate decision from storing them: assets meant for everyone belong in a public bucket behind a CDN, while anything private wants a short-lived presigned GET.

## How Runsite handles it

Runsite's [S3-compatible object storage](https://runsite.app/services/s3-storage) speaks the standard API, so everything above is the same code with a different endpoint. `upload_file` with a `TransferConfig` in boto3, `Upload` from `@aws-sdk/lib-storage`, `aws s3 cp` at the terminal: they work once the endpoint and access keys point at Runsite, with multipart the recommended path for anything above 100 MB. Objects go up to 5 TB, and prefixes are how you organise them, since `uploads/2026/07/` is a naming convention rather than a directory. Creating a bucket, issuing a key pair, and pointing an SDK at the endpoint is walked through in the [Runsite docs](https://docs.runsite.app).

Direct browser uploads are covered by presigned URLs and per-bucket CORS configuration. Access keys carry a read, write, delete, or admin scope and rotate without downtime, so signing uploads with a write-scoped key keeps the blast radius of a leaked URL inside a boundary you drew yourself. Private buckets stay private; public ones are served through a global CDN with automatic cache headers.

Transfer isn't metered in either direction. Accepting an upload costs nothing whatever its size, and re-sending a 2 GB transfer that died at 90% costs nothing either, which takes the sting out of the retry logic above. Stored gigabytes are the only variable on the invoice, at €0.025 per GB per month after 5 GB free as of writing. Objects are encrypted at rest with AES-256 and in transit with TLS, they live in the German region, and a signed GDPR Data Processing Agreement comes with every plan. User uploads from an app running on [Runsite Web Services](https://runsite.app/services/web-services) land in the same account behind the same keys, which is the common case this whole article describes.

## The short version

Generate the object key on the server, keep it opaque, and put the user's filename in your database instead, because renaming an object means copying it. Use a single PUT for small predictable files and multipart above roughly 100 MB, remembering that part size × 10,000 caps the file you can send, and that you split a file to make failures cheap rather than to go faster. Abort incomplete uploads, by lifecycle rule if your provider supports one, or pay to store parts you can't see. Managed transfer helpers need credentials, so direct browser multipart means your server signs each part and your client tracks them. Retries are safe for whole objects and individual parts, an ETag ending in `-N` proves nothing about content, and the object only counts as real once your server has confirmed it with a `HeadObject`. To push a file through the whole path before committing to it, [5 GB is free](https://runsite.app/services/s3-storage), which is enough to sign a URL, upload in parts, and watch the key appear.

Related service

## S3 Object Storage on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore S3 Object Storage](https://runsite.app/services/s3-storage)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### How do I upload a file to S3 from a web application?

There are two shapes. The application can receive the file and write it to the bucket with its own credentials, using an SDK call such as boto3's upload\_file or PutObject in the AWS SDK for JavaScript. Or the browser can write to the bucket directly using a presigned URL your server signs, which keeps large files out of your application's memory and request timeout. In both cases the server should generate the object key rather than accepting the browser's filename, keep the original name in the database for display, and treat any file over roughly 100 MB as a multipart upload so a dropped connection costs one part instead of the whole transfer.

### When do I need a multipart upload, and what part size should I use?

A single PutObject is capped at 5 GB on AWS as of writing, so anything larger requires multipart. In practice the threshold is lower: AWS recommends multipart above roughly 100 MB, because a failed single PUT restarts from zero while a failed part costs only that part. Part numbers run from 1 to 10,000, each part except the last must be at least 5 MiB, and no part may exceed 5 GiB. That gives you the sizing rule directly: part size multiplied by 10,000 is the largest file that part size can carry, so 5 MiB parts top out near 50 GB and reaching the 5 TB object limit needs parts of at least 500 MiB. Between 8 and 64 MiB works for most application uploads.

### What should I use as the object key for a user upload?

Generate it on the server, from an opaque random identifier such as a UUID or ULID, behind a prefix that reflects how you will need to list objects later, for example user/42/2026/07/. Keep the file extension if clients infer a content type from it, but treat it as a hint rather than a fact about the bytes. Store the user's original filename in your database and serve it back with a Content-Disposition header at download time. Avoid using the client's filename as the key: keys are case-sensitive, collide easily, may need escaping in URLs, and are limited to 1024 bytes of UTF-8\. The decision is effectively permanent, because object storage has no rename, and changing a key means copying the object and deleting the original.

### Can I use the ETag to verify a file uploaded to S3?

Only for objects uploaded in a single PUT without SSE-C or SSE-KMS encryption, where the ETag happens to equal the MD5 of the body. For a multipart upload the ETag is a hash over the concatenated part hashes with a dash and the part count appended, so an ETag ending in -14 means the object arrived in fourteen parts and says nothing about its contents. It also changes with the part size used, so the same file uploaded with different settings yields different ETags. For verification you can act on, send Content-MD5 with a single PUT, use the additional checksum algorithms such as CRC32, CRC32C, SHA-1 or SHA-256 where your provider supports them, or compute your own checksum and store it alongside the file.

Keep reading

---

# S3 Alternatives Without Egress Fees: What to Check

> Zero-egress object storage moves the meter, it doesn't remove it. What providers bill instead of transfer, and which workloads really save by leaving S3.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. AWS S3 Alternatives Without Egress Fees: What to Check Before You Switch

StorageAugust 5, 202611 min read

# AWS S3 Alternatives Without Egress Fees: What to Check Before You Switch

Zero-egress storage moves the meter, it doesn't remove it. What providers bill instead of transfer, why the saving only appears when bytes leave from the bucket, and which workloads actually get cheaper by leaving S3.

## The short version

Zero-egress pricing moves the meter rather than removing it, so read the operation charges, the minimum storage duration and the minimum billable object size. The saving only appears when bytes leave from the bucket rather than from your application server. A request-heavy workload can cost more on free transfer than on metered S3.

[Teo Marquardt](https://runsite.app/about#author)

You've worked out where the object storage bill actually comes from, discovered it's mostly [transfer rather than storage](https://runsite.app/blog/s3-egress-fees), and gone looking for an S3 alternative that doesn't charge for it. There's no shortage of candidates. The problem is the shape of the answer you get back: every comparison ranks providers on the headline price per gigabyte and the phrase "no egress fees," and those are the two numbers that tell you the least about what the invoice will say.

A provider that doesn't meter transfer still has to make money somewhere, and the meter it uses instead is the one to go looking for. So this is about the meters: what replaces egress on the bill, why the saving sometimes fails to appear at all, which workloads genuinely get cheaper by leaving S3, and what stops working when the bucket moves. What "S3-compatible" guarantees at the protocol level is a separate question, answered in [how S3-compatible object storage works](https://runsite.app/blog/s3-compatible-object-storage).

## "No egress fees" is a pricing shape, not a gift

Bandwidth costs the provider something. Dropping the egress line is a decision to recover that cost differently, or a bet that most customers store far more than they serve. Either way the invoice still has to add up, and four meters do the adding. Any one of them can be the reason a cheaper provider produces a bigger bill:

* **Per-operation charges.** Requests are billed by the million, usually split into a write-and-list class and a read class. [Cloudflare R2](https://developers.cloudflare.com/r2/pricing/) calls these Class A and Class B; [Backblaze B2](https://www.backblaze.com/cloud-storage/pricing) uses transaction classes. The important property is that this meter runs on the \*number\* of objects you touch, not on how big they are, which makes it invisible in any comparison done per gigabyte.
* **Minimum storage duration.** An object deleted before it has been stored for N days is still billed for the remaining days. This is standard on cold and archive tiers everywhere, and it also applies to some flat-rate providers on their normal tier: [Wasabi](https://wasabi.com/pricing) has applied a 90-day minimum on standard storage as of writing. Store something for a week under a 90-day minimum and you paid for twelve.
* **Minimum billable object size.** An object below the floor is billed as if it were the floor size. Half a million 3 KB records can be billed as if they were something much larger, and again nothing about the per-gigabyte price predicts it.
* **Free transfer with conditions attached.** Free can mean unconditional, or capped at a multiple of what you store, or governed by an acceptable-use policy, or available only over a particular path such as a partner CDN or a peering arrangement. Backblaze B2 offers free egress up to roughly three times your average stored data as of writing, metered above that. Wasabi ties free transfer to your stored volume through its use policy.

The fourth one deserves a specific question: is free transfer a term you can point at, or a policy someone can reinterpret? A cap expressed as a multiple of stored data is arithmetic, and you can check today whether your traffic fits under it. A fair-use clause is a judgement about your account made by someone else, at a moment you don't choose. Both are legitimate ways to run a business, and neither is a reason to distrust a provider. They're a reason to know which one you're buying before your traffic triples.

## The saving only exists if the bytes leave from the bucket

Before comparing anyone's prices, find out where your data physically exits to the internet, because that is the only place an egress charge can land. This one check reorders most migration plans, and it's the part the comparison articles skip.

On AWS, moving data between S3 and EC2 in the same region isn't billed as [data transfer out](https://aws.amazon.com/s3/pricing/) at the time of writing. The meter runs when bytes head for the public internet. So picture the common architecture: a request arrives at your application, the application reads the object from the bucket, and the application streams it back to the user. The S3-to-application hop was already free. The application-to-user hop is the one on the invoice. Now move that bucket to a provider that charges nothing for transfer. The hop that just became free is the hop that was free already, and the metered hop hasn't moved an inch. You did a migration and saved nothing.

The saving turns up when the storage provider hands bytes to the user directly, with your application out of the byte path: a [presigned URL](https://runsite.app/blog/s3-presigned-urls) so the browser fetches the file from the bucket itself, or a public bucket served through the provider's CDN. Then the transfer leaves from storage that doesn't meter transfer, and the line genuinely disappears. Same provider, same prices, completely different outcome, decided entirely by which machine is holding the socket.

Follow the bytes before you compare prices

Draw the path a downloaded file takes from disk to browser and mark the point where it crosses onto the public internet. Whoever owns that point is the one billing you for transfer. Zero-egress storage pays off when that point is the bucket, and changes nothing when it's your application server.

There's a second consequence for the reads your application still has to do itself: thumbnailing, virus scanning, zipping an export, serving a private file it has to authorise first. If the bucket now sits with a different provider than your compute, that read crosses a network boundary. Most clouds don't charge for data coming in, so as of writing this usually costs latency rather than money. Keeping storage and [the service that reads from it](https://runsite.app/services/web-services) close together still matters, because a few extra milliseconds on every object read has a way of showing up in page load times rather than on the invoice.

## Which meter dominates depends on the shape of your data

No provider is cheapest in general. One is cheapest for the way your particular data behaves. Work out which meter your data runs hottest and the shortlist mostly writes itself:

| Workload shape                                                                            | Meter that dominates                              | What to check on the pricing page                                                                               |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Video and large media: few objects, many bytes, served repeatedly                         | Transfer, then storage per GB                     | Whether free transfer is unconditional or capped, and whether the CDN in front is included or billed separately |
| Millions of small objects: thumbnails, map tiles, telemetry, per-user documents           | Operations, plus any minimum billable object size | Price per million reads and per million writes, and the floor size an object is billed at                       |
| High churn: CI artefacts, temporary renders, staging data written and deleted within days | Minimum storage duration                          | Whether early deletion is charged, for how many days, and whether it applies to the standard tier               |
| Backups and archives: written once, read rarely, kept for years                           | Storage per GB, plus retrieval                    | Restore charges, retrieval latency, and whether the archive tier carries its own minimum duration               |

The cheapest provider depends on which meter your workload runs hottest. Per-gigabyte price only decides the first row.

The second row is where the assumption breaks. A bucket holding fifty million thumbnails might be only a couple of terabytes, so the storage line looks trivial and the transfer line looks like the thing to optimise. But every page view reads dozens of those objects, and on a provider that bills per operation, that read meter runs on all of them. For workloads like that, free transfer can still land above metered S3, and the only way to find out is to count requests rather than gigabytes. Media is the opposite case, and the arithmetic that makes it so painful is worked through in [S3 egress fees](https://runsite.app/blog/s3-egress-fees).

## What the zero-egress providers actually bill for

With that in hand, here are the main options sorted by billing model rather than by price. Prices change and free tiers get restructured; the model each provider chose is the part that stays put long enough to compare.

| Provider                                                         | Transfer out                                                                                   | What else is metered                                                                                                                            | The line to read                                               |
| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Amazon S3                                                        | Metered to the internet past a small monthly allowance; free to AWS compute in the same region | Storage per GB, requests, storage-class transitions and retrievals                                                                              | How much of your traffic actually leaves the region            |
| Cloudflare R2                                                    | Not metered to the internet                                                                    | Storage per GB, Class A (write and list) and Class B (read) operations; the infrequent-access class adds a minimum duration and a retrieval fee | Your monthly operation counts, not your gigabytes              |
| Backblaze B2                                                     | Free up to a multiple of the data you store, metered above it                                  | Storage per GB, transaction classes                                                                                                             | Where that free allowance sits relative to your actual traffic |
| Wasabi                                                           | No separate transfer charge, bounded by an acceptable-use policy tied to stored volume         | Storage per GB, with a minimum storage duration and a minimum billable object size                                                              | The minimum-duration clause and the wording of the use policy  |
| EU providers (Hetzner, Scaleway, OVHcloud)                       | Usually an included allowance, metered above it                                                | Storage per GB, sometimes requests                                                                                                              | Size of the included allowance and which regions are on offer  |
| [Self-hosted: Garage, SeaweedFS, Ceph](https://runsite.app/blog/minio-alternatives) | No provider charge; you pay your own bandwidth                                                 | Servers, disks, replication, and your team's time                                                                                               | The operational cost you just moved in-house                   |
| Runsite                                                          | Not metered in either direction, with no allowance to exceed                                   | Storage per GB after the free tier                                                                                                              | Nothing further: operations aren't billed                      |

Billing models as of writing, not a price comparison. Confirm current terms and figures on each provider's own pricing page before you commit data to it.

Pricing pages move faster than articles do

Every figure and policy above is current at the time of writing and none of it is a substitute for the provider's own pricing page on the day you decide. Use this to know which questions to ask, then get the numbers from the source, and read the terms rather than the marketing headline for anything described as free.

## What you leave behind isn't the API, it's the identity model

"Will my code still run?" is the question everyone asks about compatibility, and for the core read-write path the answer is usually yes: change the endpoint, change the keys, and the SDK calls are the same calls. The things that don't come with you live outside the S3 API, which is exactly why a feature comparison of storage products never mentions them.

### Static keys instead of IAM roles

Inside AWS, a well-configured application holds no storage credentials at all. It runs under an [IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id%5Froles.html), and the SDK collects temporary credentials from the instance or task metadata and refreshes them on its own. Nothing is written to a config file and nothing needs rotating by hand. Point that same SDK at an S3-compatible endpoint and there's no role for it to assume: authentication is Signature Version 4 with a long-lived access key and secret that you now store, scope, and rotate yourself.

python

```
import os
import boto3

# Inside AWS: no credentials in the code at all. The SDK picks up
# temporary ones from the instance role and refreshes them itself.
s3 = boto3.client("s3")

# Anywhere else: a long-lived key pair that is now your responsibility.
s3 = boto3.client(
    "s3",
    endpoint_url=os.environ["S3_ENDPOINT"],
    aws_access_key_id=os.environ["S3_ACCESS_KEY"],
    aws_secret_access_key=os.environ["S3_SECRET_KEY"],
)
```

None of that blocks a migration, but it does change your threat model, and the answer is worth setting up on day one: a separate key per application, scoped to the narrowest permission that application needs, kept in your platform's secret storage rather than the repository, and rotated on a schedule you'll actually keep. A key that can only write to one bucket is a small incident when it leaks; an admin key pasted into every environment is a large one. Decide it during the migration, because the version of this decision that gets made under time pressure is one admin key everywhere, and that is difficult to unpick later.

### The integrations that don't travel

Bucket events triggering a Lambda, Athena querying objects where they sit, CloudFront with origin access control, Glacier tiers for cold data: these are integrations with the rest of AWS rather than with the S3 API. A provider can implement `PutObject` flawlessly and still have nothing to offer here, because the missing piece was never storage. Where those wires exist, moving the bucket means rebuilding them, and that work belongs in the migration estimate rather than in the surprise column. The gaps inside the API itself, meaning versioning, lifecycle rules, object lock and bucket policies, vary by provider and are better tested on a live bucket than read about; there's a checklist for that in [how S3-compatible object storage works](https://runsite.app/blog/s3-compatible-object-storage).

## When staying on S3 is the right answer

Sometimes the honest conclusion is that the migration isn't worth doing. A few situations where the answer is to stay put:

* **Your compute lives in AWS and the bytes leave from there.** Per the byte-path argument above, moving the bucket relocates a charge you weren't paying. Moving the delivery path is the change that would save money, and that's a different project.
* **You depend on something that only exists inside AWS.** Object Lock to satisfy a retention regime, Glacier economics for a genuinely cold archive, query-in-place over your own data, an event pipeline that other systems consume. Each of those has to be rebuilt or replaced, and the replacement is rarely free.
* **Your transfer volume is small.** An internal tool, a private bucket written far more often than it's read, an archive nobody touches. If the egress line is a few dollars, the entire prize is a few dollars.
* **Nobody wants to own key rotation.** If static credentials would sit unrotated in a config file for two years, the security cost of leaving may outrun the billing saving.

A workable rule: if transfer isn't among the three largest lines on your bill, migrating is work without a payoff. Open last month's invoice and rank the lines before you rank the providers. If transfer is up there, moving to [object storage that doesn't meter it](https://runsite.app/services/s3-storage) is the change that pays for itself.

## Running both buckets while you move

Copy everything across, flip the endpoint, delete the old bucket. That plan works fine for a bucket nobody is writing to. For a live one, the copy is out of date the moment it starts, so the sequence needs a window where both buckets are real: backfill what already exists, write new objects to both providers while the backfill runs, keep reads on the old bucket until the new one is provably complete, then move reads over and hold the old bucket read-only for as long as you'd want to roll back. Standard tools such as `aws s3 sync` and `rclone` speak both endpoints, and the step-by-step is in the [Runsite docs](https://docs.runsite.app) rather than here.

Two things reliably go wrong in that window. The first is verification: comparing ETags across providers doesn't prove anything, because the ETag of a multipart object is a hash over its part hashes and depends on the part size the copying tool happened to pick, so the same bytes yield different ETags on each side. Verify with object size plus a checksum you compute yourself, for the reasons set out in [uploading files to S3](https://runsite.app/blog/upload-files-to-s3). The second is metadata: content type, cache control and content disposition are properties of each object, and a careless copy resets them to defaults. The symptom arrives later as browsers downloading your images instead of displaying them.

One piece of good news on the way out. The cost of extracting your data used to be its own obstacle, and it's easing: AWS has offered [free data transfer out to customers leaving the platform](https://aws.amazon.com/blogs/aws/free-data-transfer-out-to-internet-when-moving-out-of-aws/) entirely since 2024, subject to a support request and its conditions, and the other large clouds have made comparable moves under regulatory pressure. The background to that shift, and why exit pricing is worth checking before you arrive rather than when you leave, is covered in [S3 egress fees](https://runsite.app/blog/s3-egress-fees).

## How Runsite reads on these axes

For a concrete answer to the questions above: [Runsite's S3-compatible object storage](https://runsite.app/services/s3-storage) doesn't meter transfer in either direction, with no allowance to exceed and no use policy converting free into conditional. Operations aren't billed either, and the free plan says so in as many words: unlimited operations. That leaves stored gigabytes as the only variable, at €0.025 per GB per month after 5 GB free as of writing, with one storage class and no minimum duration to price around. The dashboard reports storage, bandwidth and request counts in real time with alerts on thresholds, so the meters are visible whether or not they're billed.

On the rest: the API is S3-compatible, so the AWS SDK, boto3 or aws-cli work with the endpoint and keys swapped and no other change. Objects reach 5 TB and total storage is uncapped. Access keys carry read, write, delete or admin scope and rotate without downtime, which is the answer to the static-key problem above. Presigned URLs and per-bucket CORS get the browser talking to storage directly, and public buckets are served through a global CDN, so the byte path can leave your application out of it. Objects sit in a German region encrypted with AES-256 at rest and TLS in transit, with no replication outside the EU and a signed GDPR Data Processing Agreement on every plan, which is the residency axis covered in full in [where to store EU user data](https://runsite.app/blog/gdpr-data-residency).

## The short version

Zero-egress pricing moves the meter rather than removing it, so read for operations, minimum storage duration, minimum billable object size, and the conditions attached to the word free. Check where your bytes actually cross onto the internet first, because if that point is your application server, changing storage providers saves nothing until the browser fetches from the bucket instead. Match the provider to how your data behaves, since a request-heavy workload can cost more on free transfer than on metered S3\. Budget for the identity change, since an IAM role becomes a static key you own, and for the AWS-only integrations that stay behind. If transfer isn't in your top three billing lines, staying put is a legitimate answer. And if the shortlist is down to a couple of names, the quickest way to settle it is [5 GB of free storage](https://runsite.app/services/s3-storage), your own SDK, and a weekend of real traffic.

Related service

## S3 Object Storage on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore S3 Object Storage](https://runsite.app/services/s3-storage)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What are the best AWS S3 alternatives with no egress fees?

The main S3-compatible options that drop or heavily discount transfer charges are Cloudflare R2, Backblaze B2, Wasabi, and Runsite, alongside European providers such as Hetzner, Scaleway and OVHcloud and self-hosted options like Garage, SeaweedFS and Ceph. They differ less in price than in what they bill instead of transfer: R2 charges per operation in Class A and Class B tiers, B2 gives free egress up to a multiple of your stored data, Wasabi applies a minimum storage duration and an acceptable-use policy tied to stored volume, and Runsite meters neither transfer nor operations and bills only stored gigabytes. Pick by matching those meters to your workload rather than by the per-gigabyte headline, and confirm current terms on each provider's pricing page.

### Is free egress really free?

Sometimes, and sometimes it's free with conditions. Free transfer can be unconditional, capped at a multiple of the data you store, bounded by an acceptable-use policy, or available only over a specific path such as a partner CDN. The question worth asking is whether the free allowance is a term you can point at and do arithmetic against, or a policy that can be reinterpreted for your account. Separately, dropping the egress line usually means the cost is recovered elsewhere, most often through per-operation request charges, a minimum storage duration that bills you for objects you deleted early, or a minimum billable object size that inflates the cost of many small files.

### Is an S3 alternative actually cheaper for my workload?

It depends on which meter your data runs hottest. Media workloads with large objects served repeatedly are dominated by transfer, so zero-egress pricing helps a lot. Workloads made of millions of small objects are dominated by request counts, and a provider charging per operation can end up more expensive than metered S3 for that shape. High-churn data written and deleted within days is dominated by any minimum storage duration, and archives are dominated by per-gigabyte storage and retrieval charges. Count your monthly requests as well as your gigabytes, and check where your bytes leave for the internet, since a saving on storage egress only materialises if the bucket serves users directly rather than through your application.

### How do I migrate from S3 to another provider without downtime?

Run both buckets in parallel rather than doing a single cutover copy. Backfill the existing objects with a tool that speaks both endpoints, such as aws s3 sync or rclone, write every new object to both providers while the backfill runs, keep reads on the old bucket until the new one is verified complete, then switch reads over and leave the old bucket read-only for as long as you might want to roll back. Verify with object size and your own checksum rather than by comparing ETags, since a multipart ETag depends on the part size each tool chose and will differ between providers. Copy content type and cache-control metadata deliberately, because a copy that drops them shows up later as browsers downloading files instead of rendering them.

Keep reading

---

# Git Push to Deploy: How Deployment Actually Works

> What happens between git push and live traffic: build artifacts, health-gated releases, why a rollback isn't a rebuild, and what a pipeline won't do for you.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Git Push to Deploy: How Modern Web App Deployment Actually Works

DeploymentAugust 7, 202612 min read

# Git Push to Deploy: How Modern Web App Deployment Actually Works

What actually happens between git push and live traffic: build artifacts, health-gated releases, why a rollback isn't a rebuild, and the four things a deploy pipeline can't do for you.

## The short version

Nine steps run between your push and the first request on the new code, and the pivot is step four, where a commit becomes an artifact. Configuration joins at release time rather than build time, which lets one image run in preview and production. Because the artifact persists, a rollback is a lookup, not a rebuild.

[Teo Marquardt](https://runsite.app/about#author)· Updated August 8, 2026

You push to main, go and refill the coffee, and by the time you sit down the new version is live. Somewhere in those thirty seconds a machine you have never logged into cloned your repository, worked out how to build it, built it, stored the result, started it next to the version already serving traffic, asked the new one whether it was healthy, moved requests across, and shut the old one down.

Most days you don't need to know any of that. You need it on the day the deploy succeeds and the app breaks anyway. The uploaded avatars have vanished. The migration that ran fine on your laptop has taken the API down. The rollback you assumed would be instant turns out to be a five-minute rebuild that can fail on its own terms. Every one of those surprises lives at a specific point in the pipeline, and they stop being surprising once you can name the point.

So this isn't a walkthrough of connecting a repository. It's the anatomy: what happens, in what order, and which properties of your system fall out of it.

## What actually happens between git push and live traffic

Platforms name the steps differently and split them in different places, but the shape is close to universal. Continuous deployment, in the sense hosting platforms use the phrase, means this whole sequence runs on every push to a branch you nominate, without a person in the middle:

1. **A webhook fires.** Your Git host, whether that's GitHub, GitLab or Bitbucket, tells the platform that a specific commit landed on a branch it is watching. That commit is the input to everything downstream.
2. **The code is cloned and inspected.** The platform fetches the commit and decides how to build it: from a Dockerfile in the repository if there is one, otherwise by detecting the framework from the files it finds.
3. **A build runs in a throwaway environment.** Dependencies are installed and assets are compiled, then the result is packaged up. That environment exists for the build and is discarded afterwards.
4. **The result is stored as an immutable artifact.** Usually a container image, pushed to a registry. It has an identity of its own, separate from the branch it came from.
5. **A deployment manifest is assembled.** Environment variables, secrets, the health check path, resource limits, how many instances to run. Configuration meets the artifact here, and not a moment earlier.
6. **New containers start alongside the old ones.** For a stretch of seconds, both versions of your application exist. The old one is still answering every request.
7. **The health gate opens, or it doesn't.** The platform waits for the new containers to report themselves ready. If they never do, the release stops here and the old version keeps serving.
8. **Traffic shifts.** The router in front of your app starts sending requests to the new containers.
9. **The old containers are drained and stopped.** In-flight requests finish, then those containers go away along with everything they had written to disk. An app that ignores the stop signal is killed when the wait runs out, which is where [exit code 137 on every deploy](https://runsite.app/blog/exit-code-137) comes from.

The interesting thing is where your code stops being the unit of work. Up to step four the pipeline cares about a commit. After step four it cares about an image, and the commit is just provenance. Almost everything that follows in this article is a consequence of that handoff.

## Build time and run time are two different worlds

The build produces something that has to run in more than one place. The same artifact should be able to boot as a preview environment, as staging, and as production, because that is the only way "we tested this exact thing" is a true statement. Which means the build has to stay ignorant of where it will run.

What belongs in the image is everything identical across environments: your source, the installed dependencies, compiled assets, the runtime itself. What arrives at start time is everything that differs: database connection strings, API credentials, feature flags, the port to listen on. The separation between building, releasing and running, and the rule that configuration lives in the environment rather than the code, are two of the twelve factors described at [12factor.net](https://12factor.net/build-release-run), and they have aged well enough to be the default assumption of most platforms you'll meet.

dockerfile

```
# Build time. This value is baked into an image layer. Every
# environment that runs the image gets it, changing it means a
# rebuild, and anyone who can pull the image can read it.
ENV DATABASE_URL=postgres://app:s3cret@db.internal:5432/app

# Run time. The image says nothing about the database. The
# platform injects DATABASE_URL when the container starts, so
# preview and production run the identical artifact.
```

The practical failure here is quiet. Bake configuration into the image and you no longer have one artifact promoted through environments, you have as many separate builds as you have environments. Staging and production are then two different pieces of software that happen to share a commit hash, and the sentence "it worked in staging" loses most of its meaning. Secrets baked into a layer are the sharper version of the same mistake: image layers are readable by anyone who can pull the image, and a secret committed to a layer stays in that layer even if a later instruction unsets it.

## The artifact is the part that matters

A container image is content-addressed. Alongside whatever tag you gave it, it has a digest, a hash of its contents that looks like `sha256:9b2c…`, and that digest refers to exactly those bytes and no others. Tags move. A digest doesn't. As of writing, this is how the OCI image specification defines identity, and it's the property that makes the next paragraph possible.

Because the previous release is still sitting in the registry under its own digest, going back to it is a lookup. The platform assembles a manifest pointing at the old digest, starts those containers, waits for the health gate, moves traffic. No source is fetched, no dependency is resolved, nothing is compiled. It takes about as long as a deploy that skips the build, which is to say seconds.

Compare that with the reflex most developers reach for. `git revert && git push` looks like an undo, but the pipeline treats it as an ordinary new commit: clone, resolve dependencies, build, push, release. You are asking a build to save you from a bad build. It will usually work, and it will occasionally fail for a reason that has nothing to do with your revert, because a package registry is down or a transitive dependency published a new version in the last hour. It is also minutes rather than seconds, and those minutes are happening while production is broken.

Rollback is a lookup, not a rebuild

If a platform can only rebuild from source, what it calls a rollback is really a new deploy that you hope resembles the old one. The question worth asking before you need the answer: does every release leave behind an addressable artifact, and can I point production at yesterday's without touching my repository?

## The release gate: why deploys wait for a health check

A health check on a deploy pipeline is doing a different job from the one on your monitoring dashboard. Monitoring tells you about a system that is already serving. The check in a release is a gate: the new containers get no traffic at all until they answer it, and if they never answer, the traffic never moves. This is why a deploy can fail without any user noticing. The old version was serving the whole time, and the release simply refused to complete.

It also explains a class of confusing incident. If your check only confirms that the HTTP server is listening, then an application that boots with an unreachable database will pass the gate and start returning errors to real users. If the check is honest about the dependencies the app needs to do its job, the release stalls instead and you get a failed deploy rather than an outage.

How the traffic movement itself is staged, and how the checks behind it get tuned, is its own design decision, worked through in [blue-green versus rolling deployments](https://runsite.app/blog/blue-green-vs-rolling-deployments). The principle is enough here.

A health check here is a gate, not a dashboard

Monitoring tells you about a system that is already serving traffic. A release gate decides whether it gets traffic at all. No answer from the new version means no traffic, however good the check would have looked on a graph, and a release isn't finished until that answer arrives.

## Three shapes of deploy, and what each one costs you

The pipeline above is the same everywhere, but what comes out the far end isn't. Three shapes cover most of what you'll deploy, and the useful axis between them is what exists between two requests.

| Shape                    | What gets deployed                                  | Between requests                                                                       | What to watch                                                                               |
| ------------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Long-running container   | A process, started once and kept alive              | The process stays up, holding connection pools, in-memory caches and background timers | What the platform does when the app goes quiet, and whether idle costs you money or latency |
| Static artifact on a CDN | A directory of prebuilt files, copied to edge nodes | Files sitting on disk, nothing executing                                               | Cache invalidation on release, and the point where you need a server after all              |
| Per-request function     | A handler the platform invokes on demand            | Nothing, by design                                                                     | Startup cost on the first invocation and any ceiling on execution time                      |

The same pipeline can produce any of these. The difference shows up in your bill and your latency, not in your git history.

The long-running container is what you get from [a platform that runs web apps and APIs as containers](https://runsite.app/services/web-services), and it's the default for anything with a database behind it. Its behaviour when nobody is using it varies enormously between platforms. That is a topic of its own, covered in [what cold starts are and why free-tier apps fall asleep](https://runsite.app/blog/cold-starts). Static output has no such problem because there is nothing to wake up, which is why [static site hosting with a preview URL on every pull request](https://runsite.app/services/static-sites) is the cheaper answer whenever your build produces plain files. If that describes you and you are weighing hosts, [the European alternatives to Netlify](https://runsite.app/blog/netlify-alternatives-europe) are compared by operator and by what their free tiers do at the limit. Choosing between a prebuilt site and a server for a given framework is a decision with enough nuance to deserve its own treatment.

## Four things git push will not deploy for you

The pipeline covers your application. It does not cover the state your application sits in, and this is where most genuinely bad deploy days come from.

### 1\. Database migrations are a one-way door

During a rolling release, two versions of your application run at the same time, the new containers and the ones still draining, and they talk to the same database. Not two databases, one. So any schema change you ship has to be workable for the code on its way out as well as for the code arriving.

That's the reason a rename breaks production even when the new code is correct. Rename `name` to `full_name` in one migration and the old containers, which still select `name`, start throwing errors the moment the migration lands, and they keep serving a share of your traffic until the release finishes. The standard way around it is to split the change in two and let both versions coexist in between, an approach Martin Fowler describes as [ParallelChange](https://martinfowler.com/bliki/ParallelChange.html) and which you'll also see called expand and contract:

sql

```
-- Expand: add the new column, leave the old one untouched.
ALTER TABLE users ADD COLUMN full_name text;

-- Then deploy code that writes both and reads whichever exists,
-- backfill the new column, and verify. Only in a later release,
-- once nothing reads the old column, do you contract:
ALTER TABLE users DROP COLUMN name;
```

It's more work than one migration, and it's not always necessary. A short maintenance window makes the whole problem go away, and plenty of applications can afford one. It becomes unavoidable the moment you want releases with no downtime, because that requirement is what puts two versions on the same schema in the first place.

The asymmetry is worth stating plainly: rolling your code back does not roll your schema back. A dropped column stays dropped, and the previous release, pointed at a schema it was never written for, may fail in ways that are harder to diagnose than the bug you were escaping. Who runs the migration and when is your decision to make, but the recovery path is worth deciding before you need it. What a managed database solution does for developers, and what stays yours, is covered in [the operational boundary a Postgres host draws](https://runsite.app/blog/managed-postgresql-for-developers), and the database itself lives on [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql).

### 2\. Anything you wrote to local disk

A container's writable layer is tied to that container. When the container goes, the layer goes with it, which is standard container behaviour rather than a platform limitation, and [Docker's own storage documentation](https://docs.docker.com/engine/storage/) says as much. Persisting data means putting it somewhere outside the container.

The timing is what catches people out. Files written next to the application don't disappear gradually or on some cleanup schedule. They disappear at step nine, the moment a deploy retires the old containers. So an upload feature that writes to `./uploads` works perfectly through development, testing and the first weeks of production, and then loses every file on the day you ship an unrelated bug fix. User uploads belong in [S3-compatible object storage](https://runsite.app/services/s3-storage) from the start, and the practical side of moving them there is in [uploading user files to S3](https://runsite.app/blog/upload-files-to-s3).

### 3\. Background jobs and schedules

Your web process gets replaced on every deploy. Whatever else you run does not necessarily follow the same rules. [A worker consuming a queue or a task on a schedule](https://runsite.app/blog/cron-jobs-workers-queues) has a lifecycle of its own, and unless the platform ties them together, it can be running last week's code against this week's database. Long jobs make it worse, since a task that started before the deploy is still executing the old version after it. Scheduled work runs on [cron jobs with their own deploy lifecycle](https://runsite.app/services/cron-jobs), and it's worth knowing which version of your code is on the other end of a schedule.

### 4\. Domains, DNS, and certificates

The first time you point a custom domain at a platform you are doing a cutover, and the pipeline barely participates. The TTL on your DNS records determines how long resolvers keep handing out the old address, so some visitors reach the new deployment while others are still going to the previous host, and lowering the TTL is only useful if you do it well in advance. Certificates typically follow rather than lead: an ACME issuer has to verify that you control the domain before it will issue for it, which usually means DNS has to point at the platform first and HTTPS arrives shortly after, not simultaneously. Neither of these is triggered by a push, and both are worth doing on a quiet afternoon.

## Preview environments: the cheapest thing on this list

Once the pipeline exists, pointing it at a pull request instead of the main branch costs the platform almost nothing. Same clone, same build, same manifest, different address. What you get back is disproportionate: a reviewer can open the change instead of reading it, and the bugs that only show up with real assets and a real database surface before the merge instead of after.

It also quietly enforces the discipline from earlier. An artifact that can only run in production because its configuration is baked in cannot be spun up as a preview at all. If previews work, your build and your configuration are probably separated properly.

## What separates one platform from another

Every platform in this category will tell you it deploys from Git, and that is true and not very informative. The differences show up in the axes below, and each of them corresponds to a specific bad afternoon you can have later. Comparing named platforms against these axes is a separate exercise: the European ones are lined up against them in [what to look for in a European Heroku alternative](https://runsite.app/blog/european-heroku-alternative), two named US platforms are put through them in [Render vs Railway](https://runsite.app/blog/render-vs-railway), and the case where the platform is one you run yourself is in [self-hosted PaaS versus managed](https://runsite.app/blog/coolify-vs-managed-paas). One category sits outside these axes: platforms that deploy build output plus functions rather than a running process, which is a different shape of hosting and is taken apart in [Vercel alternatives for full-stack apps](https://runsite.app/blog/vercel-alternatives):

| Axis                 | The question to ask                                                        | When it bites                                                                     |
| -------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Reproducible builds  | Does the same commit produce the same artifact next month?                 | A rebuild of an old commit fails or behaves differently, and you can't tell why   |
| Addressable artifact | Is every release stored, and can I redeploy an old one without rebuilding? | You need to be back on yesterday's version in under a minute                      |
| Health-gated release | Does traffic wait for the new version to report ready?                     | A broken build reaches users instead of failing quietly                           |
| Config and secrets   | Are they attached at release time, or baked into the build?                | Environments drift apart, and secrets end up readable inside image layers         |
| Preview environments | Does a pull request get a running instance with no extra setup?            | Review happens by reading diffs, and integration bugs land on main                |
| Observability        | Logs, resource metrics and a deploy history you can correlate?             | Something broke at some point and nobody can say which release did it             |
| Region and residency | Where do the containers, build artifacts and logs physically sit?          | A customer, an auditor or a DPA asks and the honest answer is "several countries" |

The last row is the one most easily deferred and hardest to retrofit. Build artifacts and application logs are as much a data location question as the database is, and the reasoning behind keeping the whole stack in one jurisdiction is set out in [EU hosting for developers](https://runsite.app/blog/eu-hosting-for-developers).

## How Runsite handles it

Runsite implements the pipeline described above, so it's a reasonable concrete example of the shape. Connect a repository on GitHub, GitLab or Bitbucket and a webhook fires on every push to your configured branch. A Dockerfile is optional: the framework is detected automatically for Next.js, Django, Rails, FastAPI and others, and if you supply your own Dockerfile it is used instead, which is the escape hatch for anything unusual.

The built image is stored in Runsite's internal registry, and the deployment manifest carries environment variables, secrets and health check configuration separately from it. Releases are rolling: new containers start alongside the existing ones, traffic shifts once health checks pass, and a failed check rolls the release back to the previous healthy version rather than putting a broken build in front of users. Health check paths and intervals are configurable. Every pull request gets its own isolated environment on a unique URL without extra configuration. Rollbacks and deploy history are available on the Pro plan (€25/mo as of writing).

As of writing, a deploy takes roughly 30 seconds, with the full clone, build and release cycle typically completing in under a minute. HTTPS is automatic on the runsite.app subdomain or your own domain, and real-time logs, CPU and memory metrics and deploy history are in the dashboard. Everything runs in Germany, with containers, environment variables, build artifacts and logs staying inside the EU and a signed [GDPR data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) on every account. The entry Nano plan covers one web service at €3/mo with auto-deploy from Git and free SSL, the €5 starting credit pays for it, and active apps don't cold-start. You can [deploy web apps straight from Git on EU infrastructure](https://runsite.app/services/web-services), or start with Static Sites if your build produces plain files instead of a running process. Step-by-step setup lives in the [Runsite docs](https://docs.runsite.app).

## The short version

Nine steps run between your push and the first request to the new code, and the pivot is step four, where a commit becomes an artifact. Configuration joins that artifact at release time, not in the build, which is what lets one image run in preview and production. Because the artifact persists, a rollback is a lookup rather than a rebuild, and a platform that can only rebuild from source can't really offer one. The health check gates the release: no answer from the new version, no traffic. And [four things stay yours](https://runsite.app/blog/paas-vs-iaas-responsibility) no matter how good the pipeline is, namely your schema, anything written to local disk, your background workers, and your DNS.

The fastest way to internalise all of this is to watch it happen once. Connect a repository to [a platform that builds and releases on every push](https://runsite.app/services/web-services), push a commit, and read the build log while it runs. Everything above is in there, with timestamps.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What actually happens when you git push to deploy?

A webhook from your Git host tells the platform a commit landed on the branch it watches. The platform clones that commit, works out how to build it from a Dockerfile or by detecting the framework, and runs a build in a throwaway environment. The result is stored as an immutable artifact, usually a container image in a registry. A deployment manifest then attaches environment variables, secrets and the health check configuration to that artifact, new containers start alongside the ones already serving, and traffic only shifts once the new version reports itself healthy. The old containers are drained and stopped last. The key transition is the artifact: after it exists, the pipeline no longer deals in commits.

### How do I roll back a deployment?

Point production at the previously built artifact rather than rebuilding from source. Because a container image is addressed by a digest of its contents and the previous release is still in the registry, redeploying it is a lookup: assemble a manifest against the old digest, start the containers, wait for the health check, shift traffic. It takes seconds. Running git revert and pushing looks like an undo but is treated as an ordinary new commit, so it triggers a full clone, dependency resolution and build. That takes minutes while production is broken, and it can fail for unrelated reasons such as a package registry outage. If your platform can only rebuild from source, it does not really support rollback.

### Do database migrations run automatically on deploy?

That depends on the platform and on how you have wired it, and it is worth confirming rather than assuming. The more important point is that migrations behave differently from code regardless of who triggers them. During a rolling release two versions of your application run against the same database, so a schema change has to work for the outgoing code as well as the incoming code. A rename that drops the old column immediately will break the containers still serving the old version. Splitting the change into expand and contract phases, adding the new column first and removing the old one in a later release, is the usual way around it. Rolling code back does not roll a schema back.

### Why do uploaded files disappear after a deploy?

Because they were written to the container's own filesystem. A container's writable layer lives and dies with that container, so anything your application saved next to itself is discarded when the deploy retires the old containers. This is standard container behaviour rather than a platform quirk. The reason it surprises people is the timing: the files survive restarts and weeks of production traffic, then vanish during an unrelated release. User uploads, generated files and anything else that has to outlive a deploy belong in object storage or on a volume that exists independently of the container.

Keep reading

---

# 8 European Heroku Alternatives in 2026, Checked by Operator

> Scalingo, Clever Cloud, Upsun, IONOS, Sliplane, Runsite, Coolify and Infomaniak compared for Heroku users — who operates each, and what breaks in a migration.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. 8 European Heroku Alternatives in 2026 — and How to Check One Really Is European

DeploymentAugust 7, 202615 min read

# 8 European Heroku Alternatives in 2026 — and How to Check One Really Is European

Eight Heroku alternatives operated by European companies, checked on the column the lists leave out: which company you contract with, and where it is registered.

## The short version

Scalingo is the closest to Heroku's shape and the easiest to defend in a compliance review. Clever Cloud has the widest managed catalogue; Upsun suits teams wanting a clonable environment per branch. Before picking, test the word European against three separate claims: EU servers, which anyone can satisfy, EU incorporation, and a published subprocessor list.

[Teo Marquardt](https://runsite.app/about#author)· Updated August 29, 2026

Either the bill went up, or somebody filling in a security questionnaire asked where the data physically sits. Whichever it was, you have ended up reading a list of Heroku alternatives, and the lists all have the same gap: they rank platforms without checking the one property the word "European" is supposed to guarantee — which company you are actually contracting with, and where that company is registered. This page names eight and checks that column for each.

Those lists answer a question you don't have. You aren't picking your first platform. You have a running application with a database attached to it, a Redis add-on, a nightly job, and a review-app flow your team has got used to, and what you need is to know what happens to each of those pieces. If the reason you're moving is European, there's a second thing you need, because that word means at least three different things and only one of them is easy to verify.

It helps to know why there are so many of these lists in the first place. Heroku [retired its free plans in November 2022](https://blog.heroku.com/next-chapter), which pushed a generation of side projects and student apps off the platform within a few weeks and produced most of the comparison posts still ranking today. If your application has been on a paid plan the whole time, that history doesn't apply to you, but it does explain why so much of the available advice is written for hobby projects. Free tiers elsewhere carry their own conditions, and the condition to read first is what happens when the app goes idle, which is its own subject in [what cold starts are and why free-tier apps fall asleep](https://runsite.app/blog/cold-starts).

So this is the other half of the comparison: what you are actually replacing, how to test a claim of being European, and which parts of the move genuinely break.

## What you're actually replacing when you leave Heroku

Heroku's product was never the dyno. It was the bundle: a runtime, a build system that worked out what your application was without being told, a marketplace where a database was one click and one line on the same invoice, a scheduler, and a review-app flow. Leaving replaces one bundle with a set of separate decisions, and it's the number of decisions rather than the difficulty of any one of them that people underestimate.

| What you have on Heroku    | What it really is                                                          | What it becomes elsewhere                                                                           |
| -------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Dyno                       | A process running your code, sized by a plan name                          | A container sized by CPU and memory. The naming goes away and you pick numbers instead              |
| Buildpack                  | A build recipe chosen by inspecting your repository                        | Framework auto-detection on platforms that offer it, or a Dockerfile you write once and own         |
| Procfile and process types | A declaration of which commands to run and what to call them               | One configured service per line: a web service, a background worker, a step in your deploy sequence |
| Add-on marketplace         | A catalogue of third-party services, provisioned and billed through Heroku | Nothing. No other platform has this. Each add-on becomes its own decision                           |
| Heroku Postgres            | A managed PostgreSQL instance with backups and a connection string         | Managed PostgreSQL from your new platform or a separate provider                                    |
| Heroku Redis               | A managed Redis instance, usually a cache or a job queue                   | Managed Redis, with a decision to make about persistence you may never have made before             |
| Heroku Scheduler           | A cron-like runner for periodic commands                                   | A scheduled job, with its own deploy lifecycle to think about                                       |
| Review apps                | A running instance per pull request                                        | Preview environments, which most modern platforms have and some charge for                          |
| Config vars                | Environment variables injected at start time                               | Environment variables injected at start time. This part travels unchanged                           |

The migration map. Most rows are a straight swap; the add-on marketplace is the row that turns into a project.

Read the table as one claim: everything except the marketplace is a swap, and the marketplace is the work. A dyno is a container, a buildpack is a build recipe you now name explicitly, a `Procfile` line is a service you configure, and `DATABASE_URL` is still `DATABASE_URL`. What has no equivalent anywhere is the catalogue: the mail sender, the log drain, the error tracker, the search index and the metrics dashboard that were each one click and one line on a single invoice become separate providers, each with its own contract and its own bill. Count them before you start, because that count, rather than the size of your codebase, is what determines how long the migration takes.

The upside nobody mentions is that most teams find add-ons in that list they stopped using two years ago and kept paying for, because cancelling one was never urgent enough to schedule.

## "European" means three different things

Every platform selling to European developers now says it is European, and the claim is doing three separate jobs at once. They are not equally hard to satisfy, and vendors tend to satisfy the easy one and let you assume the others.

### 1\. The servers are in the EU

The weakest of the three, because any provider on earth can open a Frankfurt region. Heroku has offered an EU region in its Common Runtime for years, as of writing, and so do AWS, Google Cloud and Azure. If a region were sufficient, nobody would need this article. What a region buys you is latency and a defensible answer about where bytes are written; what it does not touch is who the operating company answers to, which is the subject of the next two.

It's still worth confirming rather than assuming, and confirming means the whole stack rather than the application container. Build artifacts, logs, backups and metrics are data too, and they are the parts most likely to sit somewhere else by default. The reasoning behind treating them the same way as the database is set out in [EU hosting for developers](https://runsite.app/blog/eu-hosting-for-developers).

### 2\. The operating company is incorporated in the EU

This is the one that changes the legal picture, because it decides which government can compel the company and under which law. A US-incorporated provider running EU servers is subject to US law regardless of where the disks are, and the instability that follows from that is the subject of [Schrems II and data transfers](https://runsite.app/blog/schrems-ii-data-transfers). Heroku is a Salesforce product, and Salesforce is a US company, which is a plain fact about corporate structure rather than a judgement about the platform.

Europe is also not the same shape as the EU, and this catches out several lists. The United Kingdom left the EU in 2020 and now sits under a separate adequacy arrangement that has been adopted and extended rather than made permanent, so a UK-incorporated platform is European in the everyday sense and third-country in the regulatory one. Switzerland is outside the EU too, with its own adequacy decision and its own data protection act. Neither of those is a reason to rule a provider out, and both are a reason to know which column you are in before an auditor asks. Check the company registration on the vendor's own imprint or terms rather than inferring it from the region list, and check the adequacy status on the day you read this rather than trusting a blog post, including this one.

### 3\. The control plane, the support desk and the subprocessors are in the EU

The quietest of the three and the one that most often fails. Your containers can run in Frankfurt while the dashboard that manages them, the support engineer who can read your logs during a ticket, the error tracker, the analytics on the marketing site and the payment processor all sit elsewhere. Each of those is a subprocessor, and a provider that takes GDPR seriously publishes the list, which is why the list is the fastest way to test the claim. A short list with EU names is a good sign. A long list of US SaaS tools with a Frankfurt region at the bottom tells you what kind of European you are dealing with.

Three questions, and where the answers actually live

Which legal entity am I contracting with, and where is it registered? Look at the imprint, the terms of service or the DPA, not the pricing page. Where do build artifacts, logs and backups live, not just the application? Look at the documentation, or open a ticket and keep the reply. Who are your subprocessors and where are they? Look for a published list; if there isn't one, that is itself the answer.

Latency, incidentally, is the axis where the answer is simplest and the least contested: distance is physics, and a European application serving European users is faster from a European region whoever owns it. That argument, along with the cost of crossing a border, is worked through in [EU vs US hosting](https://runsite.app/blog/eu-vs-us-hosting), so it isn't repeated here.

## 8 European Heroku alternatives in 2026

The table gives the operating company first, because that is the answer a security questionnaire is asking for, and what each thing actually is second, because two of these eight are not hosting companies at all. Most rows are companies established in the EU. Two are not, and they are in the table rather than excluded from it: the column tells you which is which so you can decide whether the distinction matters to your situation, which is a judgement no list can make for you. Our own row is one of the two, and it is marked as such.

| Platform                          | Operator registered in                              | What it is                             | Best for                                                                                  |
| --------------------------------- | --------------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- |
| Scalingo                          | France                                              | Managed PaaS                           | The closest thing to Heroku’s shape, and the easiest to defend in a compliance review     |
| Clever Cloud                      | France                                              | Managed PaaS                           | The broadest managed catalogue of the eight — runtimes, databases, storage, observability |
| Upsun (Platform.sh)               | France                                              | Managed PaaS                           | Teams that want a full clonable environment, data included, per branch                    |
| IONOS Deploy Now                  | Germany                                             | Managed deploys on a large German host | Teams that already buy other infrastructure from a big German provider                    |
| Sliplane                          | Germany                                             | Managed container hosting              | Running Docker containers on German hardware without operating the host                   |
| Runsite                           | Not incorporated — sole operator, EU infrastructure | Managed PaaS                           | A fixed monthly bill, EU-only data, and a DPA on every account                            |
| Coolify or Dokploy on EU hardware | Open source — you rent the server                   | Self-hosted control plane              | The smallest invoice, if you are willing to own the box                                   |
| Infomaniak                        | Switzerland                                         | Managed cloud and hosting              | Swiss rather than EU data residency, which is a different answer — see below              |

Eight European Heroku alternatives, as of writing. The first column is the one a procurement review reads; the rest is how you narrow it down.

### 1\. Scalingo — the closest to Heroku’s shape

A French company running a managed platform deliberately built along Heroku’s lines: push a repository, get a build, attach a managed database as an add-on, scale by container size. If your team’s muscle memory is Heroku’s, this is the platform where the fewest habits have to be unlearned. It is also the one assembled most explicitly around surviving a procurement review — European operating company, European infrastructure underneath, certifications aimed at buyers who must document where regulated data sits. If the reason you are moving is the security questionnaire rather than the invoice, start here.

### 2\. Clever Cloud — the widest managed catalogue

Also French, and the closest of the eight to a full platform in the way Heroku was a full platform: application runtimes, Docker deployments, managed databases, object storage and observability under one operator, billed per second so cost tracks load rather than a fixed instance size. If what you are grieving is the bundle — the fact that Heroku had an answer for every piece and now you are assembling one from vendors — this is the row that rebuilds most of it inside a single contract.

### 3\. Upsun — an environment per branch, data included

Upsun is the current product of Platform.sh SAS, registered in Paris, and it is the one row here that improves on a Heroku habit rather than merely replacing it. Review apps on Heroku gave a branch its own deployment; Upsun gives a branch its own environment with a copy of the data, which is the version your team probably wanted and did not have. The cost of that is a mental model to learn and a billing structure unlike any other row — you buy a project and allocate resources inside it, rather than paying per running container — so model your actual branch count before signing anything. For a solo application it is more machinery than the job needs.

### 4\. IONOS Deploy Now — a big German host with a deploy pipeline on top

IONOS is one of the largest German hosting companies, and Deploy Now is its git-driven deployment product. The appeal is not that it is the most refined developer experience in this table — it is not — but that for a company already buying domains, mail or servers from a large German provider, it collapses one more vendor into an existing contract and an existing invoice. That matters more to some finance departments than any feature in this article.

### 5\. Sliplane — containers on German hardware, without operating the host

Sliplane is a Berlin company offering managed container hosting: you bring a Docker image or a repository, it runs on German infrastructure, and you do not administer the machine. It sits deliberately between the self-hosted row and the full managed platforms — fewer managed services than Clever Cloud, a smaller bill than most, and no operating system on your maintenance list. Worth a look if what you actually want is “my container, running in Germany, and nothing else”.

### 6\. Runsite — a fixed bill and a DPA on every account

Ours, and the limits first so you can weigh them. Runsite is not a registered company: it is operated by an individual developer, so on the incorporation column above it gives a different answer from Scalingo SAS or Clever Cloud SAS, and if your procurement review needs a company registration number in an EU member state, one of those is your answer rather than this one. There is one region, in Germany, so an application with users in São Paulo or Singapore will serve them from Europe. Plans are a flat monthly figure rather than a meter, which is the wrong shape for something that idles six days a week. And there is no migration tool: the dyno-to-container work described further down is yours. What is here is the Heroku bundle rebuilt on EU-only infrastructure — [web services](https://runsite.app/services/web-services), [PostgreSQL](https://runsite.app/services/postgresql), [Redis](https://runsite.app/services/redis) and [object storage](https://runsite.app/services/s3-storage), each with an entry plan of €5/mo or less, so the add-ons you are losing have somewhere to land without adding three vendors. Everything runs in Germany, nothing replicates outside the EU, and a signed [data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) comes with every account.

### 7\. Coolify or Dokploy on European hardware — the smallest invoice

The odd row out, because there is no company to name. Coolify and Dokploy are software you install on a machine you rent from a European provider, and what they give back is the shape of the thing you are leaving: push a branch, get a deploy, click to add a database. The bill is the price of the box. The catch belongs in this article specifically rather than in a general cost comparison: with no operator, you are the processor. The data processing agreement your customer asks for is one you write about yourself, the incident response is yours, and so is the patching of the control plane that makes the whole thing feel managed. Dokku, Kamal and CapRover trade more of the dashboard for less to maintain. The hours behind the saving are counted in [what self-hosting a PaaS actually costs](https://runsite.app/blog/coolify-vs-managed-paas).

### 8\. Infomaniak — Swiss, which is not the same as EU

Infomaniak is a Geneva company and Switzerland’s largest cloud provider, with a genuine privacy and sustainability record and data centres on Swiss soil. It appears on every European alternatives list, and it is worth being precise about what it answers: Switzerland is outside the EU, so a transfer of personal data there is a transfer to a third country, currently permitted under an adequacy decision. That is a good answer and a different answer from “the data never leaves the union”. Whether the distinction matters depends entirely on what your customer’s questionnaire actually asks, and adequacy decisions are the kind of thing that gets revisited — the history is in [Schrems II and EU data transfers](https://runsite.app/blog/schrems-ii-data-transfers).

The ones that look European and are not

Three groups appear on European alternatives lists while answering the jurisdiction question differently from the rows above, and the difference is worth stating rather than hiding. Northflank is operated by Northflank Ltd in London: the United Kingdom, outside the EU since Brexit, currently covered by an adequacy decision — the same footnote as Switzerland. Render, Railway and Fly.io all offer a Frankfurt or Amsterdam region and are all US companies; the region moves the servers, not the jurisdiction. And Heroku itself is Salesforce, which is where you started. None of these is a bad platform, and for many teams none of this decides anything. It decides something when a contract, an auditor or a customer questionnaire asks which law governs the data — and in that case the useful move is to read the provider’s own imprint and terms rather than any list, including this one.

## Three shapes, and who gets paged at 3am

Cutting the same eight a second way, because the country of the operator does not tell you what you are signing up to run. There are three shapes here, and the axis that separates them is who gets paged when something breaks at three in the morning.

| Shape                                       | Examples                                                                                | What you pay with                          | Who fixes it at 3am                                            |
| ------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------- |
| Managed EU PaaS                             | Clever Cloud, Scalingo, Upsun, Runsite                                                  | Money, and some flexibility                | The provider. You get an incident page and a support ticket    |
| Self-hosted PaaS on European infrastructure | Coolify, Dokploy, Dokku or CapRover on Hetzner, Scaleway, OVHcloud, Exoscale or UpCloud | Your time, on a schedule you don't control | You. The PaaS layer is now yours to patch, back up and upgrade |
| Plain EU servers and your own pipeline      | A VPS and CI of your choosing                                                           | Your time and your risk tolerance          | You, with nothing between the incident and your terminal       |

Three ways to run an application in Europe. Named platforms are examples of each shape as of writing, not a shortlist or a ranking.

The middle row is the one that tempts developers who have just read a bill, because a self-hosted PaaS on a €5 server does look like a fully featured platform for the price of a coffee. It is a genuinely good option, and the cost it moves rather than removes is operational: the machine, the PaaS layer running on it, the database on it and the backups of that database are all now on your maintenance list. That trade is worked through in full in [what you take on with Coolify and a self-hosted PaaS](https://runsite.app/blog/coolify-vs-managed-paas). The bottom row removes even the PaaS layer, which is a different calculation again, and mostly a question of how much of a deploy pipeline you want to maintain by hand after reading [what a deploy pipeline actually does](https://runsite.app/blog/git-push-to-deploy).

One thing all three rows share: none of them is Heroku, so none of them will feel like Heroku on the first day. The bundle is the thing you're giving up, and no single replacement rebuilds it.

## What actually breaks in the migration

The code usually moves without complaint. What breaks is the set of things Heroku was quietly doing on your behalf, and each of them fails in a way that looks like a bug in your application.

### The variables you never set yourself

Heroku populates a handful of environment variables for you, and the first deploy elsewhere is where you find out which ones your code assumed. `PORT` is the usual first failure: the platform assigns it, your application binds to it, and an application that binds a hardcoded port will start, look healthy in its own logs and never receive a request. `DATABASE_URL` and `REDIS_URL` appeared when you attached the add-ons, so on the new platform they are yours to set and yours to keep in sync when the database moves. Forget one, and the client falls back to its default, which is how a missing `REDIS_URL` turns into [ECONNREFUSED 127.0.0.1:6379](https://runsite.app/blog/econnrefused-127-0-0-1) on the first request.

bash

```
# Heroku set these. Nothing on the other side will.
PORT=...          # assigned by the platform; read it, never hardcode it
DATABASE_URL=...  # appeared when you attached Heroku Postgres
REDIS_URL=...     # appeared when you attached Heroku Redis

# Worth grepping for before you move:
#   any literal port number in your server bootstrap
#   any add-on variable you didn't set by hand
#   anything reading a *_URL you can't point at in your own dashboard
```

### The Procfile becomes configuration

A [Procfile](https://devcenter.heroku.com/articles/procfile) is a short file with a lot of convention behind it: Heroku knows that the process type named `web` is the one that gets HTTP traffic, that `release` runs before the new version goes live, and that everything else is a worker. Elsewhere, each line turns into something you configure explicitly, and the `release` line is the one to look at hardest, because that is usually where your migrations run.

yaml

```
# Heroku: three lines, and the platform knows what they mean.
web: gunicorn app.wsgi
worker: celery -A app worker
release: python manage.py migrate

# Elsewhere: three separate things you set up.
#   web     -> a web service with a health check path
#   worker  -> a background service with its own deploy lifecycle
#   release -> a step you place deliberately in the deploy sequence,
#              because nothing runs it for you by convention
```

Where that `release` step ends up matters more than it looks, for reasons that have nothing to do with Heroku: during a rolling release two versions of your application talk to the same database, so a migration has to be workable for the outgoing code as well as the incoming code. That constraint, and the expand-and-contract pattern that satisfies it, is covered in [git push to deploy](https://runsite.app/blog/git-push-to-deploy). It's the same on both sides of the migration, which is exactly why moving is a good moment to check you were getting it right.

### The build stops guessing

Buildpacks inspect a repository and infer the rest, which is pleasant right up to the point where you don't know what was inferred. Some platforms detect frameworks the same way and you carry on as before. Some hand you a Dockerfile, which means every implicit choice becomes an explicit line: the runtime version, the system packages someone added through a custom buildpack in 2021, the asset compilation step that ran because a file happened to exist. This is more control and more homework, and the homework is front-loaded. It's also worth knowing that the buildpack idea outlived Heroku's implementation of it as [Cloud Native Buildpacks](https://buildpacks.io/), a CNCF project, so "no buildpacks" and "a Dockerfile forever" are not the same statement.

### The bill changes shape, which makes price comparison misleading

Heroku bills dyno hours against a plan name. Most replacements bill a container by the CPU and memory you allocate it. Those are different units, so putting two monthly figures side by side compares nothing useful until you know what your application actually consumes, and most teams find they were paying for a plan tier chosen years ago rather than for the resources in use. Get the memory and CPU numbers off your current metrics first, then price against those.

Price the workload, not the plan name

A dyno plan and a container size are different units of account, so comparing the two monthly figures tells you very little. Take the actual CPU and memory your application uses from your existing metrics, price a container that size on each candidate, then add every add-on you're replacing as its own line. That total is the comparable number, and it is often lower than expected on both sides for the same reason: the old plan was sized by guesswork. One small app priced that way on seven platforms is in [PaaS pricing compared](https://runsite.app/blog/paas-pricing-compared).

### And the parts that don't break

The container filesystem behaves identically, which is to say anything your application wrote next to itself still disappears when the release retires the old container. That was already true on Heroku, so nothing gets worse. Rolling releases and health-gated traffic work the way they always did, and a rollback still depends on whether the platform kept the previous artifact rather than on which platform you're using. Your `DATABASE_URL` convention survives, and so does every twelve-factor habit the Heroku era taught you. That's the pleasant irony of this migration: the platform that popularised those habits left your application portable.

## The order to move things in

The sequence that goes wrong is the one that starts with the database, because the database is the piece that can't be run twice. Start with the stateless things instead. Get the application building and running on the new platform against the old database, if your network rules allow it, or against a copy, and leave production traffic exactly where it is. That first deploy is where you find the hardcoded port, the missing environment variable and the buildpack that was doing something you didn't know about, and finding them costs nothing while nobody is watching.

Add-ons come next, one at a time, because each is an independent decision and independent decisions are safest made in isolation. Background jobs and scheduled tasks follow, with one thing to watch: a scheduler running on both platforms at once will send two of every email. Turn the old one off in the same change that turns the new one on. The database goes last and is the only step with a real cutover, which means a brief window where writes stop, a final replication or dump, and a switch of the connection string. Everything before that point is reversible by leaving the old platform running, and the day you delete the Heroku app should be at least a week after the day you stop using it.

That's the shape of it rather than the commands. The database step with the actual commands, including the restore errors a Heroku dump throws and a cutover order that doesn't lose writes, is in [migrating off Heroku Postgres](https://runsite.app/blog/migrate-from-heroku); the setup for a specific application on Runsite lives in the [Runsite docs](https://docs.runsite.app).

## When staying on Heroku is the right answer

A migration is real work, and there are cases where the honest recommendation is not to do it. There is also a fourth shape of answer that this list leaves out, which is not moving to a platform at all: [what a bare European server actually costs once you itemise it](https://runsite.app/blog/vps-vs-paas-cost) is the version of this decision with no platform on either side. The cases for staying put:

* **You're deep in the Salesforce ecosystem.** If Heroku Connect is synchronising objects into your database, the platform has become part of your integration architecture rather than a place your application happens to run, and replacing it is a different project with a different budget.
* **Your add-on list is long and load-bearing.** Ten add-ons is ten migrations. If they're all in daily use, price the total effort honestly before comparing monthly bills.
* **The bill is small.** A hobby project on a low tier is not where infrastructure time pays for itself. Move it when it becomes something you'd be upset to lose.
* **Nobody owns the move.** A half-finished migration, with a database on one platform and workers on another, is worse than either endpoint. If there's no week for it in the next quarter, wait for the week.

The one case where waiting is expensive is a compliance requirement with a date attached, because the DPA and the subprocessor answers are needed before the deal closes rather than after, and no amount of engineering speed compresses a procurement review.

## How Runsite reads on these axes

For a concrete answer against the three tests above: Runsite runs in Germany, and the containers, environment variables, build artifacts and logs stay inside the EU rather than the application alone. A signed GDPR Data Processing Agreement comes with every account instead of being an enterprise upgrade. Those are the first and third tests; the second is a question about the operating entity, and the right place to check it is the imprint and the DPA, exactly as recommended for any other vendor on this page.

On the migration map: connect a repository on GitHub, GitLab or Bitbucket and a webhook builds on every push to your branch. The framework is detected automatically for Next.js, Django, Rails, FastAPI and others, which is the closest thing to a buildpack, and your own Dockerfile is used instead when you supply one. Any language that runs in a container works, so Node.js, Python, Go, Ruby, Rust, Java, PHP and .NET all deploy the same way. Releases are rolling with a configurable health check gating traffic, and a failed check returns to the previous healthy version rather than putting a broken build in front of users. Every pull request gets its own environment on its own URL with no extra configuration, which is the review-app row. Rollbacks and deploy history are on the Pro plan (€25/mo as of writing). A deploy takes around 30 seconds, with the full clone, build and release cycle usually under a minute, and HTTPS is automatic on the runsite.app subdomain or your own domain.

For the add-on rows, the pieces are separate services on the same account and the same invoice: [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql) from €3/mo, [managed Redis for caches and job queues](https://runsite.app/services/redis) from €5/mo, [scheduled jobs with their own deploy lifecycle](https://runsite.app/services/cron-jobs), object storage with no egress charges, and transactional email with 3,000 messages a month included. That is not a marketplace and doesn't pretend to be one; anything outside that list stays a decision you make yourself, per the table at the top. The entry Nano plan is one web service at 0.1 vCPU and 256 MB for €3/mo with auto-deploy and SSL, covered by the €5 starting credit, and it doesn't cold-start while the app is active, which makes it a reasonable place to run the first test deploy described above. Details are on the [EU-hosted platform that builds and releases on every push](https://runsite.app/services/web-services).

## The short version

You're replacing a bundle rather than a host. Every row of that bundle except the add-on marketplace is a straight swap, and the marketplace is where the work actually lives, so count your add-ons before you estimate anything. Then test the word European against three separate claims: servers in the EU, which any provider can satisfy; incorporation, which decides whose law applies; and the subprocessor list, which is where the claim usually comes apart. That test run against two named US platforms, both of which offer an EU region, is in [Render versus Railway](https://runsite.app/blog/render-vs-railway). Sort your candidates by shape rather than by rank, because managed, self-hosted and do-it-yourself differ mainly in who is awake at three in the morning. And expect the platform's invisible favours to break first. `PORT`, the add-on variables, the `Procfile` conventions. The container filesystem and your migrations behave exactly as they did before.

The cheapest way to find out what your own migration involves is to run it once, badly, with nothing at stake: point a copy of the application at [a European platform that deploys straight from Git](https://runsite.app/services/web-services), deploy it, and read what fails. That list is your actual migration plan, and it takes an afternoon to produce.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is the European alternative to Heroku?

There isn't a single one, and the useful answer depends on how much you want to operate yourself. Eight in common use, with the country of the operating company, which is the property that decides whose law applies: Scalingo (France), Clever Cloud (France), Upsun, which is Platform.sh SAS in Paris, IONOS Deploy Now (Germany), Sliplane (Germany), Coolify or Dokploy self-hosted on European hardware, and Infomaniak (Switzerland, which is outside the EU and covered by an adequacy decision rather than by EU law directly). Runsite, which publishes this page, runs on EU-only infrastructure in Germany but is operated by an individual rather than a registered company, so it answers that column differently from the French and German operators above. Northflank is frequently listed as European and is registered in London, which since Brexit is the United Kingdom rather than the EU. On the managed platforms the provider runs the platform and you deploy to it. A self-hosted PaaS such as Coolify, Dokploy, Dokku or CapRover on European infrastructure from Hetzner, Scaleway, OVHcloud, Exoscale or UpCloud gives the same push-to-deploy workflow for a lower bill, in exchange for you patching, backing up and upgrading the platform layer. Plain servers with your own pipeline is the third option. Before comparing features, check what each provider means by European: servers in an EU region is the easiest claim to satisfy and says nothing about which law the operating company is subject to.

### Is there a free Heroku alternative for small projects?

Several platforms still offer a free tier, and the thing to read is what happens when the app is idle rather than the headline. Some suspend the container after a period of inactivity, which means the next visitor waits for it to start again; some cap monthly hours; some restrict the free tier to static output rather than a running server. Free managed databases are usually the tighter constraint, since they tend to carry size limits, time limits or both. Runsite has no free web tier as of writing: the entry Nano plan covers one web service at 0.1 vCPU and 256 MB for €3/mo, which the €5 starting credit pays for, with auto-deploy from Git and free SSL, and it doesn't cold-start while the application is active, sleeping only after 14 days idle. For anything you'd be upset to lose, read the data retention terms before the performance ones.

### What do I need to replace when I migrate off Heroku?

Work through the bundle piece by piece. The dyno becomes a container sized by CPU and memory; the buildpack becomes framework auto-detection or a Dockerfile you own; each Procfile line becomes a service you configure explicitly, with the release line being the one that runs your migrations; Heroku Postgres and Heroku Redis become managed instances from your new platform or a separate provider; Heroku Scheduler becomes a scheduled job; review apps become preview environments; and config vars carry over unchanged. The row without an equivalent anywhere is the add-on marketplace, so every add-on you use becomes its own provider, contract and bill. The length of that add-on list, rather than the size of your codebase, is what determines how long the migration takes.

### Does hosting in an EU region make a provider European?

No. An EU region determines where bytes are written, which helps with latency and is a reasonable thing to require, but it says nothing about which government can compel the company that operates the service. A US-incorporated provider running servers in Frankfurt remains subject to US law. Two further checks separate a genuine European provider from a European region: which legal entity you are contracting with and where it is registered, which appears in the imprint, terms or DPA rather than on the pricing page, and who the subprocessors are and where they sit, which a provider taking GDPR seriously publishes as a list. Note also that Europe is wider than the EU: the UK and Switzerland sit outside it under separate adequacy arrangements whose status is worth checking on the day rather than assuming.

Keep reading

---

# Coolify vs Managed PaaS: What Self-Hosting Costs You

> Running Coolify on your own box is a real option with a real bill: the layers you now own, the single failure domain you built, and where GDPR moves.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Coolify and Self-Hosted PaaS vs Managed: What You're Taking On

DeploymentAugust 10, 202613 min read

# Coolify and Self-Hosted PaaS vs Managed: What You're Taking On

What you actually take on when the PaaS layer runs on your own server: the maintenance ledger, the one box that holds everything, and when it stops paying off.

## The short version

A self-hosted PaaS hands you a control plane, which is one layer out of six. The operating system, the Docker daemon and its disk, the proxy and its certificates, and the panel itself stay yours, and they generate real work no invoice records. It wins on per-service cost past roughly a dozen services.

[Teo Marquardt](https://runsite.app/about#author)

It took an afternoon. A server at a European provider for the price of a couple of coffees, one install command, a dashboard on a subdomain, a repository connected, and a push that came back green. You now have push-to-deploy, preview environments, a Postgres container and TLS certificates that renewed themselves, and the monthly bill went from something with a comma in it to five euros. The natural next thought is that you have been overpaying for years.

That first afternoon is real, and nothing in this article is going to argue you out of it. Coolify is a good piece of software doing a job people need doing, and the same is true of Dokploy, Dokku and the rest of that shelf. The question here is a different one, and it only shows up later: what exactly did you take responsibility for when you ran that install command?

Search for Coolify alternatives and you get the wrong shape of page. Every result compares one self-hosted panel with another self-hosted panel, so the implied question is always "which one do I install", never "should this layer be mine at all". That second question has a real answer, it varies by situation, and for a decent number of people the answer is that self-hosting wins. Working out which group you're in means looking at what actually sits on that server.

## What Coolify actually is, and what it isn't

A self-hosted PaaS is a control plane. It watches your Git host, builds images, starts containers, wires up a reverse proxy and shows you all of it in one dashboard. As of writing, [Coolify](https://coolify.io/) describes itself as an open-source, self-hostable alternative to platforms like Heroku, Netlify and Vercel, which is an accurate description of what it replaces and a slightly misleading one about what it removes. A managed platform is a control plane plus the operations underneath it. Install the control plane on your own machine and you have bought the first half.

That distinction is the whole article, so here it is concretely. This is what sits between your `git push` and a served request when the panel is yours, and who owns each layer. [The European Heroku alternative](https://runsite.app/blog/european-heroku-alternative) sorted the options into three shapes and left the middle one for later; this is that middle row, opened up.

| Layer                         | Who owns it once you self-host                                      | What it looks like when it's neglected                                                          |
| ----------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Hardware and network          | Your IaaS provider (Hetzner, Scaleway, OVHcloud, Exoscale, UpCloud) | A hardware incident is handled for you, but the recovery of what was running is not             |
| Operating system              | You                                                                 | Unpatched kernel and packages on a machine with a public dashboard on it                        |
| Docker daemon and its storage | You                                                                 | The disk fills with old images and orphaned volumes, and everything stops at once               |
| Reverse proxy and TLS         | The panel configures it, you own the outcome                        | A renewal that quietly failed, or a routing change that took the dashboard offline with the app |
| The PaaS control plane itself | You                                                                 | A version behind on a component that holds your deploy credentials and Git tokens               |
| Your application and its data | You, on both sides of the fence                                     | The same as anywhere else, except nobody else is watching the graphs                            |

Six layers, one of which the panel covers. The right-hand column is the part that never appears in a comparison table.

One row of six is what the panel takes off your plate, and it's the row you were most conscious of, which is why the trade feels better than it is. The layers above and below it were being handled by somebody before, and the handover happened silently. This is the same accounting problem [self-hosted versus managed PostgreSQL](https://runsite.app/blog/self-hosted-vs-managed-postgresql) works through for a database: a sticker price you can see, sitting on top of an operational cost you can't. The method there applies here unchanged, so it isn't repeated below. What is different is the ledger, because a control plane fails in ways a database doesn't.

## The ledger nobody prices: maintaining the control plane

You can see the shape of this ledger in what people search for after they install. Not "how do I deploy", which the panel solved, but how to update it, how to back it up, how to configure the proxy in front of it, why a certificate stopped renewing, and how to keep the dashboard from being reachable by everyone. Those aren't beginner questions. They're the maintenance surface of the layer you adopted, and each one is an afternoon that arrives without warning.

### Updating the platform while it serves production

This is the one with no equivalent on a managed platform, and it's awkward in a way that's hard to design around. Your applications get updated by the control plane. The control plane gets updated by you, by hand, on a machine that is serving live traffic at the time. There is no health-gated rollout for the thing that performs health-gated rollouts, and if an upgrade goes sideways you are debugging the deployment system with the deployment system offline. What that gate does for your own applications, and what the release strategy behind it costs, is covered in [blue-green versus rolling deployments](https://runsite.app/blog/blue-green-vs-rolling-deployments).

In practice most upgrades are uneventful and take ten minutes. The problem is that "most" is doing load-bearing work in that sentence, and the exceptions land on a schedule you don't pick. The reflex response is to postpone, which is exactly how you end up several versions behind on the component holding your deploy keys, your Git tokens and your environment variables.

### The proxy, the certificates, and the panel you put on the internet

The panel sets up a reverse proxy and requests certificates, and for a single application with a single domain that really is automatic. It stays automatic until you want something the default template doesn't cover: a wildcard certificate, a second domain on the same service, a websocket route, a header the upstream needs. Then you are editing proxy configuration, and the thing you are editing sits in the request path of every application on the box, including the dashboard you'd use to fix it.

Certificates deserve their own line because their failure mode is delayed. [Let's Encrypt](https://letsencrypt.org/docs/faq/) issues short-lived certificates by design and expects renewal to be automated, which means a renewal can break in March and you find out in May. Nothing alerts you unless you set up the alert, and the alert is now yours to set up. The dashboard itself is the uncomfortable part: it holds credentials for your Git host, your registries and every environment you deploy to, and by default it wants to be reachable from wherever you are. Putting it behind a VPN or an allowlist is the right answer and it is a piece of work with no relationship to shipping your product.

### Disk pressure, and the backup that lives on the backed-up machine

Every build leaves an image behind. Every container that ever ran may have left a volume. Docker does not reclaim any of it until you ask, which is why [its own storage documentation](https://docs.docker.com/engine/manage-resources/pruning/) has a page on pruning, and on a small server the disk is the resource that runs out first, quietly, over about four months. When it goes, it doesn't degrade politely: builds fail, the database refuses writes, and the panel that would tell you what happened is on the same volume.

Which leads to the backup question, and the answer people give too quickly. Yes, the panel can take scheduled backups. The thing to check is where they're written. A backup on the same disk as the thing it protects covers exactly one failure mode, the one where you delete something on purpose, and none of the others. Getting them off the machine means an external target, credentials for it, a retention policy, and a restore you have actually performed. [Object storage that isn't on the machine you're backing up](https://runsite.app/services/s3-storage) is the cheap part of that list; the restore drill is the part everyone skips.

## One box, one failure domain

Self-hosting a PaaS puts every part of your system into a single failure domain, and that is the structural cost of it. The control plane, the reverse proxy, the application containers, the database, the built images and the volumes are all on one machine, usually one disk, under one operating system, reachable through one IP address. A managed setup separates those into different systems because they were built by different teams in different places. Your setup consolidates them because consolidating them is what made it cheap.

text

```
# One €5 server, after four months of ordinary use.
#
#   the control plane      the thing that would fix the others
#   the reverse proxy      in the request path of everything, dashboard included
#   app containers         prod and staging, because why pay twice
#   the database           on the same disk as the app writing to it
#   built images           every release you might want to roll back to
#   named volumes          including the ones nothing has mounted since March
#   the backups            written locally, because that was the default
#
# Shared: one disk, one kernel, one reboot, one IP.
# Single-tenant is a synonym for single point of failure.
```

Consolidation has a specific consequence for rollback that carries over from [how a deploy pipeline actually works](https://runsite.app/blog/git-push-to-deploy). There, going back to yesterday's release is a lookup: the previous image still exists in a registry, so you point at its digest and start it. On a single box that property holds only while the images are still on the disk, and the disk is the resource under pressure. Every guide to reclaiming space on a full Docker host tells you to prune unused images. Unused images are your rollback targets. The cleanup that saves you on Tuesday removes the escape route you need on Thursday.

Here, rollback is a disk question

A managed platform keeps old releases in a registry that is not your application's disk, so rolling back stays a lookup no matter how full your server gets. Self-hosted, the artifact history and the running system compete for the same gigabytes, and the standard fix for a full disk deletes the history. Worth deciding in advance how many releases back you want to be able to go, and reserving the space for them on purpose.

The recursion is the sharpest version of the same idea. The system that recovers your applications cannot recover itself. If the machine is unreachable, the dashboard is unreachable, and you are back to SSH and whatever you remember about how it was configured. That is a survivable position for someone who set it up last month and a much worse one eighteen months later, on a Sunday, when the person who set it up has left. Putting the database on [managed PostgreSQL in a separate failure domain](https://runsite.app/services/postgresql) is the cheapest way to break the pattern without giving up the panel, because it moves the one component whose loss is permanent.

## Where the GDPR line actually moves

Self-hosting on a German or French server is often framed as the compliance-safe option, and there is something to that: the data sits in the EU on infrastructure operated by an EU company, and the reasoning for wanting that is set out in [EU hosting for developers](https://runsite.app/blog/eu-hosting-for-developers). What changes, and what nobody mentions, is the shape of the paperwork underneath it. You haven't removed a party from the arrangement. You've replaced them with yourself.

* **Your DPA now covers the metal, not the platform.** The [data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) with your IaaS provider covers what they operate, which is the server. The layer above it is software you administer, so there is no processor to hold accountable for it, because that role is now yours. [Where the responsibility line falls between IaaS and PaaS](https://runsite.app/blog/paas-vs-iaas-responsibility) works through all three versions of that boundary, including the legal one.
* **Breach notification stays with you and gets harder.** The GDPR's 72-hour window runs from awareness. With a managed provider, part of your detection capability is their obligation to tell you. Self-hosted, awareness means your own logging and your own alerting on a machine whose logs are on the disk that just failed.
* **Logs, build artifacts and backups are yours to place.** These are personal data as readily as the database is. On your own server they land wherever the defaults put them, which is fine while the default is local and becomes a question the moment you point backups at somewhere cheaper.
* **The subprocessor list is one you maintain.** Every service the stack touches, from an error tracker to an email relay, is now something you added and something you have to be able to enumerate when a customer's security review asks.

None of that makes self-hosting non-compliant, and it would be dishonest to imply otherwise. Plenty of teams run it properly. It does mean the compliance work moved rather than disappeared, and if the reason you self-hosted was a procurement question about data location, it's worth checking that the answer you now give is one you can evidence. Treat this as the shape of the problem rather than legal advice, and check it against your own counsel before it matters.

## What self-hosting genuinely buys you

A comparison that only pointed one way wouldn't be worth reading, and this one doesn't. There are situations where running the panel yourself is the correct engineering decision, not a compromise:

* **The economics invert with the number of applications.** One service on a managed platform is priced sensibly. Fifteen small services, half of them internal tools nobody outside the company will ever hit, are much cheaper on one box you already maintain. The per-service cost of a managed platform is its weakest point, and this is where it shows.
* **Arbitrary stacks run without asking permission.** A Docker Compose file with six services, a piece of software with unusual networking requirements, a container that needs a device on the host. Managed platforms constrain what you can deploy in exchange for operating it. Self-hosted has no such constraint, which is also why the failure surface is larger.
* **One-click templates cover ground a platform won't.** Self-hosted panels ship large catalogues of ready-to-run open-source applications, which is a different product from a place to deploy your own code. If half of what you run is off-the-shelf software, that catalogue is the feature.
* **Internal and homelab workloads have different stakes.** A staging environment, an internal dashboard or a personal project has no revenue attached and no customer to notify. The failure domain argument above still applies; it just costs a lot less when it fires.
* **Open source is insurance against terms changing.** You can read the code, fork it and keep running the version you have. For teams that have been burned by a licence change, this is not an abstract benefit.

One more signal, and it comes from the self-hosted side of the argument. As of writing, Coolify also sells a hosted edition of itself, where they run the control plane and you don't. That's the same software, minus the six-layer table above, and the fact that the option exists at all is a fair signal about how much of the value was in the panel and how much was in not operating it.

## The rest of the shelf, briefly

If you've decided the layer should be yours, the choice of which panel is a smaller decision than it looks, and a fast-moving one, so a feature matrix here would be stale by winter. As of writing: Coolify and Dokploy are where the attention has gone, with Dokploy built around Docker Swarm from the start and therefore easier to spread across more than one node. Dokku is the oldest of them and the most Unix-shaped, driven from the command line with a small footprint. CapRover is a mature Swarm-based panel that has been quieter lately. Kamal, from 37signals, skips the dashboard entirely and deploys containers from a YAML file, which is a different bet: less to run, less to look at. Easypanel is the one people pick for the interface.

Any of them will run your applications. None of them changes the ledger in this article, because the ledger belongs to the server rather than to the panel. Removing the panel and building the pipeline by hand is a further step in the same direction, with its own arithmetic, and it deserves separate treatment. If the question is not whether to host the layer yourself but where to take an existing project that already runs on a managed platform, [what leaving Railway actually detaches](https://runsite.app/blog/railway-alternatives) covers that move instead.

## Where the line falls

The useful question isn't which option is cheaper, because the answer to that depends on numbers you can look up in ten minutes. It's what you have more of: services, or people who can fix a server. Those two variables put you in one of a few positions. If you are still one step back from this and wondering whether the control plane needs to exist at all, [the cost of a bare server against a managed platform](https://runsite.app/blog/vps-vs-paas-cost) runs the same ledger with no panel in the middle.

| Your situation                                      | What the cost says                             | What the failure domain says                               | Reasonable answer                                               |
| --------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------- |
| One or two production services, one developer       | The saving is small in absolute terms          | Everything you have is on one machine you administer alone | Managed. The bill is smaller than the first incident            |
| A dozen services, someone who enjoys infrastructure | Self-hosting wins clearly on per-service cost  | Real, but there is somebody whose job includes noticing    | Self-hosted, with backups off the box and the panel not exposed |
| A dozen services, one developer, no time            | Self-hosting looks like the obvious call       | One person, one machine, and no second pair of eyes        | Split it. Keep the box, move the database and backups off it    |
| Internal tools, staging, homelab                    | Managed pricing per service is hard to justify | An outage costs an afternoon, not a customer               | Self-hosted, without much agonising                             |

The uncomfortable cell is the third one, because the economics and the risk point in opposite directions.

The third row is where most of the regret lives. The spreadsheet says self-host, the workload says self-host, and the thing that eventually goes wrong is not the platform but the absence of anyone with the time to notice it going wrong. Splitting the difference works better than picking a side: keep the panel for the applications, and move the pieces whose loss is permanent onto something you don't operate.

The question to answer before you install, not after

If this server were gone in the next ten minutes, what would it take to be serving traffic again, and have you done it once? Write down the steps, the location of the backups, and who besides you can perform them. If the honest answer runs past an afternoon or names exactly one person, the number on the invoice was never what you were choosing between.

## How Runsite handles it

For the managed side of the trade, concretely: Runsite runs the same push-to-deploy shape and keeps the six layers above off your list. Connect a repository on GitHub, GitLab or Bitbucket and a webhook builds on every push to your branch. The framework is detected automatically for Next.js, Django, Rails, FastAPI and others, and your own Dockerfile is used instead when you supply one. Releases are rolling with a configurable health check gating traffic, a failed check returns to the previous healthy version, and every pull request gets its own environment on its own URL. A deploy takes around 30 seconds as of writing, with the full clone, build and release cycle usually under a minute. HTTPS is automatic on the runsite.app subdomain or your own domain, so no proxy configuration or certificate renewal lands on you. Rollbacks and deploy history are on the Pro plan (€25/mo as of writing).

On the failure-domain question, the pieces are separate services on the same account and the same invoice rather than processes on the same disk: [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql) from €3/mo as of writing, with backups you don't operate, [managed Redis for caches and job queues](https://runsite.app/services/redis) from €5/mo, scheduled jobs, and object storage with no egress charges. Everything runs in Germany, with containers, environment variables, build artifacts and logs staying inside the EU and a signed GDPR data processing agreement on every account. That last point is the one that matters against the section above: the platform layer has a processor attached to it, and it isn't you.

The entry Nano plan is one web service at 0.1 vCPU and 256 MB for €3/mo, covered by the €5 starting credit, with auto-deploy from Git and free SSL, and it doesn't cold-start while the application is active, which makes it a reasonable place to park one service from your existing box and compare. Always-on plans start at €5/mo and go up to 8 vCPU and 16 GB. Step-by-step setup is in the [Runsite docs](https://docs.runsite.app). A server at any European provider sells raw compute more cheaply, and it always will. The price difference buys five of the six layers off your list, and a rollback history that doesn't live on the disk that fills up.

## The short version

A self-hosted PaaS gives you a control plane, which is one layer out of six. The operating system, the Docker daemon and its disk, the proxy and its certificates, the panel itself and everything your application touches stay with you, and the work they generate is real even though no invoice records it. Those layers also share one machine, which means the artifact history you'd roll back to competes for disk with the system that's running, and the tool that would repair everything else can't repair itself. Compliance doesn't vanish either; it changes hands, because the platform layer no longer has a processor behind it. Against that, self-hosting genuinely wins on per-service cost once you have a dozen of them, on arbitrary stacks, and on anything internal where an outage costs an afternoon.

So the decision comes down to two counts: how many services you're running, and how many people can fix the box. If those numbers point in opposite directions, split the difference rather than the price. Move one service onto [a European platform that builds and releases on every push](https://runsite.app/services/web-services) and compare what's left on your list, not what's left in your account.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What are the alternatives to Coolify?

They fall into two groups, and most comparisons only cover one. The self-hosted group is other panels you install on your own server: Dokploy, which is built around Docker Swarm and is the fastest-growing of them as of writing, plus Dokku, CapRover, Easypanel, and Kamal from 37signals, which drops the dashboard and deploys from a YAML file. Choosing between those changes the interface, not the responsibility, because the server, the operating system, the Docker disk, the proxy and the backups stay yours in every case. The second group is managed platforms, where a provider runs the control plane and the layers under it. European options include Clever Cloud, Scalingo, Upsun and Runsite. Which group you want depends on whether you have more services than people who can fix a server.

### Coolify or Dokploy — how do I choose?

The honest answer is that this matters less than it appears, and any feature comparison ages within a couple of releases. As of writing, the clearest structural difference is that Dokploy was built with Docker Swarm in mind, so spreading across more than one node is closer to its design, while Coolify has the larger community and template catalogue. Both give you Git-based deploys, a dashboard and one-click databases on hardware you rent. Neither changes the maintenance ledger: updating the control plane by hand while it serves production, keeping the proxy and certificates working, stopping the disk filling with old images, and getting backups off the machine they protect. If you are picking between them, pick on which interface you would rather debug at 2am, then spend the saved deliberation on where your backups are written.

### Is running Coolify on a Hetzner server cheaper than a managed platform?

On the invoice, usually yes, and the gap widens with every service you add, since one server can hold a dozen small applications while managed platforms price each one. On total cost, it depends on a number that isn't printed anywhere: the hours the server takes and what those hours would otherwise have produced. The work is specific rather than vague. Operating system patches, upgrading the control plane by hand on a machine serving live traffic, proxy configuration whenever a default doesn't fit, certificate renewals that fail silently, pruning Docker images before the disk fills, and moving backups somewhere other than the disk they protect. For a dozen services and someone who enjoys that work, self-hosting comes out ahead. For one or two production services and one developer, the saving is small in absolute terms and the exposure is not.

### Does self-hosting on a European server make me GDPR compliant?

It puts the data in the EU, which is a good start and not the same thing. The arrangement changes rather than simplifies: your data processing agreement with the infrastructure provider covers the server they operate, but the platform layer above it is software you administer, so there is no processor standing behind it. The GDPR's 72-hour breach notification window still runs from the moment you become aware, and self-hosted that awareness depends entirely on logging and alerting you set up yourself. Logs, build artifacts and backups are personal data too, and where they end up is now your decision rather than a documented property of a provider. Teams run this properly all the time. The point is that the compliance work moved onto your side of the line rather than disappearing, and it's worth confirming you can evidence the answers before a customer's security review asks for them.

Keep reading

---

# Render vs Railway 2026: Pricing, Free Tiers and EU Regions

> Per-second billing versus fixed instances, what each free tier does to an idle app, and which regions you get — plus the one thing neither pricing page decides.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Render vs Railway: Pricing, Free Tiers, and the Question Neither Answers

DeploymentAugust 12, 202612 min read

# Render vs Railway: Pricing, Free Tiers, and the Question Neither Answers

Every comparison argues per-second billing against fixed instances. That axis is real, and it is not the one that decides where your data lives.

## The short version

Railway meters what you consume; Render sells you an instance. Choosing between them is choosing which uncertainty you carry: a bill your own bug can surprise, or one that keeps charging while the app idles and that you had to size before you had data. Both are US companies, so neither settles jurisdiction.

[Teo Marquardt](https://runsite.app/about#author)

Two tabs are open. One quotes a price per second, the other a price per month, and the arithmetic refuses to resolve because it depends on traffic you don't have yet. In a third tab is a comparison article that picks a winner, and it says roughly what the four other comparison articles said.

This is not a fifth ranking. The billing difference between the two is real and the next section states it plainly, because there is no skipping it. What follows after that is the part the comparison genre leaves out, which is also the part that gets expensive later rather than at signup.

One clarification on scope first. If the question underneath your search is whether the platform layer should run on a server you administer, that is a different trade with a different ledger, and it is worked through in [self-hosted PaaS versus managed](https://runsite.app/blog/coolify-vs-managed-paas). If you have already decided to leave Railway and want to know what a project costs to unpick before it can move anywhere, that is [what leaving Railway actually detaches](https://runsite.app/blog/railway-alternatives). Everything below assumes somebody else operates the platform and you are choosing which somebody.

## The comparison everyone writes, and why it's half a decision

Search this and you get one axis, repeated. Railway meters actual consumption: as of writing [their pricing page](https://railway.com/pricing) describes charging per second for the CPU, memory and disk your services use, with each plan bundling a credit allowance and metering anything beyond it. Render sells instances at a fixed monthly rate per service. That is an accurate summary and it is where nearly every page on the subject stops.

The axes that separate one deploy platform from another were worked through in [how git push to deploy actually works](https://runsite.app/blog/git-push-to-deploy): reproducible builds, an addressable artifact, health-gated releases, where config and secrets attach, preview environments, observability, and region and residency. That post notes that applying those axes to named platforms is a separate exercise. This is that exercise. Running it turns up something a price table structurally cannot show, which is that on the last axis these two platforms give the same answer, and it is the answer most people check last.

## Two billing models are two shapes of uncertainty

Which one is cheaper has no answer that survives contact with a real workload, because the two models are not pricing the same thing. Usage billing makes the invoice a function of what your application consumed, so it tracks reality closely and moves when reality moves. Fixed instance pricing makes the invoice a function of a decision you made in advance, so it stays still while reality moves underneath it. The question with an actual answer is which of those two uncertainties you would rather carry.

|                                     | Billed on what you consumed                           | Billed on the instance you chose                         |
| ----------------------------------- | ----------------------------------------------------- | -------------------------------------------------------- |
| What the bill tracks                | Your application's actual behaviour, second by second | A capacity decision you made before you had traffic data |
| What you have to get right up front | Nothing, which is the appeal                          | The size, without knowing the load                       |
| A quiet month                       | Costs close to nothing                                | Costs exactly the same as a busy one                     |
| A bad Tuesday                       | Arrives as a number at the end of the month           | Arrives as a slow application that afternoon             |
| What it hides from you              | That a bug can be a purchase                          | That you are paying for a ceiling you never reach        |

The same trade seen from both sides. Neither column is the safe one; they are unsafe about different things.

### When usage billing surprises you

The failure mode worth knowing about is that a bug acquires a price. A retry loop with no backoff, a worker draining a queue that keeps refilling, a background job that was supposed to run nightly and is running continuously, an endpoint that got posted somewhere. On a fixed instance all of those present as a slow application, which is a symptom every developer is trained to notice within the hour. Under usage billing they present as an application working correctly, and you find out at the end of the billing period. Egress is metered on its own line in most usage models, which means a hot asset served from the wrong place is a second version of the same surprise.

### When a fixed instance wastes money

The opposite failure is quieter and more common. You are paying for the ceiling rather than the floor, so an application serving forty requests a day costs what it would cost serving forty thousand, and you had to guess the size before you had a single day of production data to guess from. Guess high and you fund headroom you never touch. Guess low and you discover it during the traffic event you sized for. Resizing is available on both, but it is a step between named tiers rather than a slope, so the correction happens after the incident rather than during it.

text

```
# One application, one month, two invoices.
#
# Metered on consumption
#   idle overnight ................ close to zero
#   normal daytime traffic ........ tracks it honestly
#   the retry loop on the 14th .... billed, silently, as legitimate work
#   the launch-day spike .......... billed, correctly, as a great day
#   -> the bill is a report on the month you actually had
#
# Fixed instance
#   idle overnight ................ full price
#   normal daytime traffic ........ full price
#   the retry loop on the 14th .... free, and visible as latency
#   the launch-day spike .......... free until it hits the ceiling, then 503
#   -> the bill is a report on a decision you made in week one
```

Neither model is a trick played on the customer. Preferring the first because you are small and spiky is sound, and preferring the second because a finance team needs a number in advance is equally sound. What is not sound is treating the choice as an arithmetic problem with one right answer, since the inputs are your own traffic pattern and your own tolerance for a surprise.

## Free tiers: what happens when nobody is using the app

The useful question about a free tier is not whether it exists but what it does to an idle application. As of writing, [Render's documentation](https://render.com/docs/free) states that it spins down a free web service after 15 minutes without inbound traffic, that bringing it back takes about a minute, and that a visitor arriving in the meantime is shown a loading page; free instance hours are also capped monthly, after which the service is suspended until the next month. Railway, as of writing, lists a standing free plan built on a small monthly credit allowance rather than on always-on hours, alongside a separate 30-day trial credit, so the binding constraint there is the allowance rather than an idle timer.

That last detail is worth dwelling on for a second, because a good deal of writing on this comparison describes Railway as trial-only. Their own pricing page as of writing lists a permanent free plan next to the trial. The summaries were out of date and the source was not, which is the general lesson: on pricing, the vendor's own page is the only citation worth having, and everything in this section should be re-read there before you rely on it.

What a spin-down costs is not measured in money. The mechanics of why an idle container takes time to answer are covered in [what cold starts are and why free-tier apps fall asleep](https://runsite.app/blog/cold-starts); the relevant point here is who waits. Not you, since you are hitting the app constantly while you build. The person who waits is whoever arrives first after a quiet period, which on a demo link in a job application or a webhook that fires twice a week is precisely the visit you cared about. A minute is a long time to hold a browser tab that has given you no reason to stay.

Three questions to ask a free tier

What happens at minute sixteen of no traffic, and who is waiting when it comes back? What happens when the monthly allowance or hour cap runs out — does the service degrade, suspend, or start charging? And what happens to the free database, which on most platforms carries a shorter clock than the free application does, so the thing that expires first is the thing holding your data.

## An EU region is not a European provider

Both platforms will run your application inside the EU. As of writing Render's region list is Oregon, Ohio, Virginia, Frankfurt and Singapore, so Frankfurt is a supported choice, and Railway offers European capacity as well. Neither fact makes either company a European provider, because a region determines where bytes are written and says nothing about which government can compel the company operating the service. The three separate things that "European" can mean, and why only one of them survives a procurement questionnaire, are set out in [choosing a European Heroku alternative](https://runsite.app/blog/european-heroku-alternative), so they are not re-derived here.

Applied to these two, the test is short. Look at which legal entity appears in the terms you are agreeing to and where that entity is registered, then look at the published subprocessor list and where those sit. Both are US companies as of writing, and a US entity operating a Frankfurt region stays subject to US law regardless of where the container runs. Why that remains true when the servers are in Germany is the substance of [Schrems II and what it did to data transfers](https://runsite.app/blog/schrems-ii-data-transfers). Run that check against their current terms rather than against this paragraph, since corporate structure is exactly the kind of fact that changes without a changelog entry.

There is a second layer here that does not come up when the platform is one you run yourself, and it catches people out. The region selector applies to the service you are creating. Your build artifacts, your deploy and application logs, your metrics history and your managed database backups are separate systems, and whether they live in the same jurisdiction as the container is a question you have to ask specifically. All of them hold personal data as readily as the database does, which is the argument made at length in [EU hosting for developers](https://runsite.app/blog/eu-hosting-for-developers). None of this implies either platform mishandles data. It means the dropdown answers a narrower question than the one an auditor is going to ask.

If neither of the two is the answer and you are looking further, [Render alternatives](https://runsite.app/blog/render-alternatives) sorts nine platforms by who operates them, and [PaaS pricing compared](https://runsite.app/blog/paas-pricing-compared) prices the same small app on seven of them, including both of these.

## The three things you only see in month three

Feature matrices go stale within a quarter, so rather than a table of what each platform ships today, here are the questions worth putting to both pricing pages before you commit. They share a property: none of them hurts in week one, and all of them are awkward to reverse.

* **A region is chosen once.** As of writing, [Render's region documentation](https://render.com/docs/regions) says the region is picked during the creation flow and that changing the region of an existing service or database is not supported, so moving means recreating the service and migrating the data underneath it. Multi-region on that model means one service per region rather than one service spread across several. Whatever the current answer is on either platform, this is the least reversible decision on the signup page, and it is usually made in the first ninety seconds.
* **The database beside the application is often the larger line.** Comparisons price the web service and stop. In practice the managed Postgres or Redis sitting next to it costs as much or more, and free database tiers tend to carry expiry dates that the free application tier does not. Price the pair, and check what happens on the day the free database's clock runs out.
* **Egress is metered separately and nobody models it.** Outbound traffic is its own line on most usage-based bills, and it is the line that scales with success rather than with your architecture. Why it lands so much harder than people expect, and what it does to the arithmetic of serving files, is worked through in [why downloading your own data costs so much](https://runsite.app/blog/s3-egress-fees).

## What each of them is genuinely good at

Railway's interface is the reason people stay. The service canvas makes an architecture visible as a graph rather than as a list of unrelated dashboard entries, environment variables can reference each other across services, and the distance between a repository and a running application is about as short as anyone has managed. Combined with consumption billing, a low-traffic or bursty workload can run there for very little. That is just what metering by the second does when the meter is honest.

Render's case is maturity. Background workers and cron jobs are service types in their own right as of writing rather than a container you configured into that shape, the platform has been doing the same thing long enough that the edges are worn smooth, and a fixed monthly figure is a thing you can put in a budget and forget. For a team whose main requirement is that nothing surprises anybody, that combination is worth real money. If either description fits your situation, nothing in this article is trying to move you.

* **Choose Railway if** your traffic is bursty or the application is idle most of the day, if you want the architecture visible as a graph rather than as a list of dashboard entries, and if you would rather pay for what ran than for what you provisioned. The trade you are accepting is that a runaway job is billed as legitimate work and you learn about it at the end of the month.
* **Choose Render if** the load is steady enough to size, if somebody needs the number in advance, or if you want background workers and cron as first-class service types rather than containers bent into that shape. The trade is that a quiet month costs what a busy one costs, and you picked the instance size before you had a day of production data.
* **Choose neither if** the question underneath the search is whose law governs your customers' data. Both are US companies as of writing, so the choice settles your billing model and leaves that question exactly where it was, in whichever region you deploy.

## Which question you're actually answering

The choice looks like a pricing comparison and behaves like two independent decisions that happen to be made on the same afternoon. One is about how you want to be billed. The other is about whose law your infrastructure sits under, and it does not appear on either pricing page. Comparing two platforms also assumes a platform is the answer, and [the cost of a raw VPS against a managed one](https://runsite.app/blog/vps-vs-paas-cost) is the option this page skips over. A third decision hides behind both if you arrived here from a frontend platform, since Vercel and its peers deploy build output and functions rather than a running process, and that gap is measured in [Vercel alternatives for full-stack apps](https://runsite.app/blog/vercel-alternatives).

| Your situation                                     | What the billing model says                                | What jurisdiction says               | Reasonable answer                                               |
| -------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------- |
| Side project, bursty traffic, no customer data yet | Consumption billing costs close to nothing at idle         | Doesn't arise yet                    | Either. Pick the interface you would rather debug               |
| Steady production traffic, load you can predict    | A fixed instance is easier to budget and harder to blow up | Ask now, while moving is still cheap | Fixed pricing, and an EU operator if you have EU users          |
| EU customers, B2B, security questionnaires coming  | Barely matters at this size                                | Decides it outright                  | A European operating entity. A region setting won't pass review |
| Leaving a US platform to reduce US exposure        | Not why you're moving                                      | The entire reason you're moving      | Moving between two US companies doesn't move the line           |

The bottom row is the one that catches people, because the migration was motivated by something the migration does not change.

The fourth row is where most of the wasted effort lives. A team decides, usually after a customer asks or a contract lands, that its stack should be under European law, and then migrates from one US platform to another because the second one has a Frankfurt region. The application moves, the data moves, the work is real, and the answer to the question that prompted it is unchanged. What does change when the operator changes, separated from what changes when only the region does, is worked through in [EU vs US hosting](https://runsite.app/blog/eu-vs-us-hosting).

Worth answering before the migration, not after

Write down the sentence you would send a customer who asks which company processes their data and under which country's law. If the honest version of that sentence names a US entity, a region setting will not change it, and a migration that only changes the region will have cost you a weekend to arrive at the same sentence. Decide whether that sentence matters to your customers before you decide which platform to move to.

## How Runsite handles it

For the European side of that trade, concretely, and starting with the part that sits awkwardly. Runsite prices fixed monthly plans rather than metering consumption, which puts it in the right-hand column of the table above with all of that column's drawbacks: a quiet month costs the same as a busy one, and you size the service before you know the load. [Web services on EU infrastructure](https://runsite.app/services/web-services) run from €3/mo for the entry Nano plan at 0.1 vCPU and 256 MB up to 8 vCPU and 16 GB as of writing. If your workload is bursty and mostly idle, consumption billing will be cheaper, and no amount of positioning changes that.

The second limitation is regions. There is one, in Germany. Against a platform offering five regions across three continents that is a shorter list, and if you need to serve Singapore from Singapore it is the wrong answer. What that single region buys is the sentence from the callout above: your containers, environment variables, build artifacts and logs stay inside the EU, there is no replication to non-EU regions, and a signed [GDPR data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) comes with every account. Everything named in it is processed on EU infrastructure, so the answer you give a customer's security review is the same answer whether they ask about the runtime, the build cache or the log store.

The rest is the shape you would expect. Connect a repository on GitHub, GitLab or Bitbucket and a webhook builds on every push to your branch; the framework is detected for Next.js, Django, Rails, FastAPI and others, and your own Dockerfile is used when you supply one. Releases are rolling behind a configurable health check, a failed check returns to the previous healthy version, and every pull request gets an environment on its own URL. A deploy takes around 30 seconds as of writing. Rollbacks and deploy history are on the Pro plan (€25/mo). Even on the entry Nano plan, active applications don't cold-start, so the loading-page problem from earlier doesn't apply; a service sleeps only after 14 days idle. Alongside it sit [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql) from €3/mo and [managed Redis for caches and job queues](https://runsite.app/services/redis) from €5/mo, and [object storage with no egress charges](https://runsite.app/services/s3-storage), which removes the third item from the month-three list entirely. Setup details are in the [Runsite docs](https://docs.runsite.app).

## The short version

Railway meters what you consume and Render sells you an instance, and picking between those two is picking which uncertainty you carry: a bill that can be surprised by your own bug, or a bill that keeps charging while the application sits idle and that you had to size before you had any data to size it with. On free tiers, ask what happens to an idle application and who waits when it wakes, and read the vendor's own page rather than a comparison, because several of the widely repeated claims about both are out of date.

Then there is the question neither pricing page raises. Both are US companies as of writing, so choosing between them settles your billing model and leaves your jurisdiction exactly where it was, whichever region you select. If that matters to your customers, it is a decision about which company you contract with rather than which dropdown you pick, and it is far cheaper to make now than after the migration. Deploying one service on [a European platform that builds and releases on every push](https://runsite.app/services/web-services) is a short way to find out whether the answer you would give a customer changes.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Which is better, Render or Railway?

Neither is better in general, and the honest split is short. Choose Railway if traffic is bursty or the application idles most of the day, if you want the architecture visible as a graph rather than a list of dashboard entries, and if you would rather pay for what ran than for what you provisioned; the cost is that a runaway job bills as legitimate work and shows up at the end of the month. Choose Render if load is steady enough to size, if a finance team needs the figure in advance, or if you want background workers and cron jobs as first-class service types; the cost is that a quiet month is priced like a busy one and you sized the instance before you had production data. There is a third case where the answer is neither. Both are US companies as of writing, so if the real question is whose law governs your customers' data, choosing between them does not touch it.

### Render vs Railway: which one is cheaper?

There is no answer that holds without knowing your traffic, because the two are not pricing the same thing. As of writing Railway meters actual consumption, charging per second for the CPU, memory and disk your services use, with each plan bundling a credit allowance and metering usage beyond it. Render sells fixed monthly instances per service. For a bursty or mostly idle workload, consumption billing usually wins, sometimes by a lot, since an idle application costs close to nothing. For steady traffic, a fixed instance is often cheaper and always easier to forecast. The two also fail differently, which matters more than the average case: under consumption billing a retry loop or a runaway worker is billed as legitimate work and shows up at the end of the month, while on a fixed instance the same bug shows up the same afternoon as a slow application. Check both pricing pages directly rather than a comparison, as the tiers on both have changed more than once.

### Do Render and Railway have a free tier?

Both do as of writing, and they are constrained in different ways. Render's documentation states that free web services spin down after 15 minutes without inbound traffic, take about a minute to come back with a loading page shown to whoever is waiting, and are capped at a monthly instance-hour allowance after which the service is suspended until the next month. Railway lists a standing free plan based on a small monthly credit allowance, separate from its 30-day trial credit, so the limit there is the allowance rather than an idle timer. Note that many comparison articles still describe Railway as trial-only; their own pricing page as of writing says otherwise, which is a reason to read the source. Two things are worth checking on either platform. Who waits when an idle application wakes up, and what happens to the free database, which on most platforms expires well before the free application does.

### Is Render a good hosting platform?

For steady production workloads it is a mature one, which is a different claim and usually the more useful one. Background workers and cron jobs are service types in their own right as of writing rather than containers configured into that shape, deploys run from Git, and a fixed monthly price per service is a number you can budget and stop thinking about. The limits worth knowing before you commit are these. A region is chosen when the service is created and Render's documentation states that changing the region of an existing service or database is not supported, so moving means recreating and migrating. Free web services spin down after 15 minutes of no traffic and take about a minute to answer again. And a fixed instance charges the same whether the application served forty requests that month or forty thousand, so you are paying for the ceiling rather than the floor.

### Is Railway good for hosting?

It is well suited to applications that are bursty, small, or still changing shape, and less suited to ones where the invoice needs to be predictable. The strengths are real: the service canvas makes an architecture legible as a graph, environment variables can reference each other across services, and metering by the second means an idle application costs close to nothing. The failure mode to plan for is the other side of that same meter. A retry loop with no backoff, a worker draining a queue that keeps refilling, or a nightly job that is running continuously all present as an application working correctly rather than as a slow one, and the bill arrives at the end of the period. Egress is metered on its own line as well. Set a budget alert on day one rather than after the first surprising month.

### Is Railway better than Vercel?

They are built for different shapes of application, so the comparison usually resolves on what you are deploying rather than on which is better. Vercel is oriented around frontend and serverless functions, with Next.js as the case it fits best. Railway runs long-lived containers, which is what you want for a backend process, a background worker, a job queue, or a database sitting next to the application, and it bills by consumption rather than per seat and function invocation. If your project is a frontend with a handful of API routes, Vercel is the shorter path. If it is a backend with services that stay running and state that needs to live somewhere, Railway is the closer fit. Note that both are US companies as of writing, so on the jurisdiction question they give the same answer. The longer version of that split, including the four things a frontend platform leaves you to buy elsewhere, is in the Runsite guide to Vercel alternatives for full-stack apps.

### What are the alternatives to Railway?

They divide into three groups and most listicles only cover the first. The first is other US-operated managed platforms in the same category, including Render, Fly.io, Heroku and Vercel, which changes your billing model and your interface but not the jurisdiction your infrastructure sits under. The second is European managed platforms, where the operating company is itself in the EU: Clever Cloud, Scalingo and Upsun are in this group, and it is the only group that changes the answer you give a customer asking whose law governs their data. Runsite, which publishes this page, belongs beside them on data residency — EU-only infrastructure, a DPA on every account — but is operated by an individual rather than an incorporated company, so on the entity question it answers differently. The third is running the platform layer yourself with Coolify, Dokploy, Dokku or Kamal on European infrastructure from Hetzner, Scaleway, OVHcloud, Exoscale or UpCloud, which is cheaper on the invoice and hands you the operating system, the Docker disk, the proxy and the backups. Which group you want depends on whether your constraint is price, convenience or jurisdiction. The longer version of that decision, including what a Railway project costs to take apart before you can move it anywhere, is in the Runsite guide to Railway alternatives.

### Does deploying to an EU region make Render or Railway a European host?

No. A region determines where the bytes are physically written, which is a reasonable thing to require and helps with latency, but it says nothing about which government can compel the company operating the service. As of writing Render offers Frankfurt among its regions and Railway offers European capacity, and both are US companies, so a US entity is operating that European region and remains subject to US law. Two checks separate a European region from a European provider: which legal entity appears in the terms you are agreeing to and where it is registered, which is in the terms or DPA rather than on the pricing page, and who the subprocessors are and where they sit. There is a second layer specific to managed platforms, which is that the region setting applies to the service you created, while build artifacts, deploy logs, metrics history and database backups are separate systems whose location you have to confirm separately. All of them hold personal data as readily as the database does.

Keep reading

---

# 9 Railway Alternatives in 2026, Compared by Operator

> Render, Fly.io, Northflank, Coolify, Clever Cloud, Scalingo, Upsun and Runsite compared for Railway users: who operates each, how it bills, what migrating costs.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. 9 Railway Alternatives in 2026 — and What You're Actually Detaching From

DeploymentAugust 13, 202614 min read

# 9 Railway Alternatives in 2026 — and What You're Actually Detaching From

Nine platforms that replace Railway, compared by who operates them and how they bill — plus the four things inside your project that decide how hard the move is.

## The short version

How hard leaving Railway is has nothing to do with which platform tops which table. Four things inside your project decide it: variables that are references rather than values, databases living inside the project boundary, a build produced without a Dockerfile, and whatever sits on an attached volume. Take those apart first, in that order.

[Teo Marquardt](https://runsite.app/about#author)· Updated September 13, 2026· Facts last checked September 13, 2026

The short version, before the reasoning. Render is the closest like-for-like move. Fly.io suits an application that has to sit near users in several regions. Northflank is the one to look at if you want production controls or your own cloud account underneath. Coolify and Dokploy on European hardware produce the smallest invoice, in exchange for owning the server. Clever Cloud, Scalingo and Upsun are the three run by companies incorporated in the EU, which is the group that changes the answer when a customer asks whose law governs their data. Runsite, which publishes this page, keeps the data in the EU but is operated by an individual rather than a company, so it answers that question differently. Each is described below, with the property that separates it from the others.

The longer version is that which of those nine is right for you depends less on the platform than on what your Railway project is holding onto — and that part is measurable before you choose anything.

This is the third list of Railway alternatives you have opened this morning, and by now the pattern is visible: each one is written by a platform, and each one comes first in its own ranking. The tables disagree about who wins and agree about the format, which is a feature matrix comparing things you will not care about in a month.

None of them answers the question you actually have. You are not choosing a platform in the abstract. You have a project on Railway that works, and the thing standing between you and any of these names is the amount of unpicking nobody has quantified for you.

Two adjacent questions are deliberately out of scope here. If you want two named managed platforms held against each other, that is done in [Render vs Railway](https://runsite.app/blog/render-vs-railway). If the real question is whether the platform layer should run on a server you administer, the ledger for that is in [self-hosted PaaS versus managed](https://runsite.app/blog/coolify-vs-managed-paas). What follows assumes you are leaving and want to know what leaving costs.

## Nine Railway alternatives in 2026, and who each one is for

The column most comparison tables leave out is the one that decides whether a migration solves the problem that prompted it. A platform is operated by a company, that company is registered somewhere, and where it is registered is what determines whose law reaches your customers’ data — not which region you picked from a dropdown. The table sorts on that, then on how each platform bills, because those are the two properties that do not change between the day you read this and the day you deploy.

| Platform            | Where the operator is registered                                                        | How it bills                                           | Best for                                                                    |
| ------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------- |
| Render              | [United States](https://render.com/terms) (Render Services, Inc.)                       | Fixed instance tiers, prorated by the second           | The closest thing to a like-for-like move                                   |
| Fly.io              | [United States](https://fly.io/legal/terms-of-service/) (Fly.io, Inc.)                  | Per-second, machines can stop when idle                | An app that must run close to users in several regions                      |
| Northflank          | [United Kingdom](https://northflank.com/legal/terms) (Northflank Ltd)                   | Per-second usage; can run in your own cloud account    | Production controls, and teams that want to keep the infrastructure account |
| Coolify             | [Open source, Apache-2.0](https://github.com/coollabsio/coolify); you rent the server   | Your server bill, nothing else                         | The smallest invoice, if you are willing to own the operations              |
| Dokploy             | [Open core, mostly Apache-2.0](https://github.com/Dokploy/dokploy); you rent the server | Your server bill, nothing else                         | A lighter self-hosted control plane than Coolify                            |
| Clever Cloud        | [France](https://clever.cloud/legal-notice/) (Clever Cloud SAS, Nantes)                 | Per-second, scales with load                           | A French operator with a broad managed-service catalogue                    |
| Scalingo            | [France](https://scalingo.com/legal-notice) (Scalingo SAS, Strasbourg)                  | Per-minute for most products, fixed container sizes    | Regulated workloads that need a French operator named on paper              |
| Upsun (Platform.sh) | [France](https://upsun.com/trust-center/legal/impressum/) (Platform.sh SAS, Paris)      | Per-project fee, plus resources and per-user licences  | Larger teams running many environments per project                          |
| Runsite             | [Not incorporated](https://runsite.app/compliance): sole operator, EU infrastructure                       | Fixed plan sizes, billed hourly from a prepaid balance | Small teams that want EU-only data and a bill that does not move            |

Nine alternatives to Railway. Each operator column links to that company's own terms or legal notice; billing was checked against its pricing page on 13 September 2026\. Prices move; operators and billing shapes rarely do.

### 1\. Render — the closest like-for-like move

Render is where most people leaving Railway end up, and the reason is that almost nothing about the workflow changes: connect a repository, get a build, get a URL, add a managed Postgres next to it. What does change is the billing model. Railway meters what you consume; Render [sells instance types at a fixed monthly price](https://render.com/pricing), prorated to the second, so the number on the invoice stops depending on how busy a bad week was. If the reason you are leaving is that the bill moved without warning, that swap alone addresses it. Its free tier [spins a web service down after 15 minutes without traffic](https://render.com/docs/free) and takes about a minute to wake it, and a free Render Postgres database expires 30 days after creation. Both are worth knowing before you put a demo link on it; the mechanics are in [why free-tier apps fall asleep](https://runsite.app/blog/cold-starts). The head-to-head is in [Render vs Railway](https://runsite.app/blog/render-vs-railway), and leaving Render itself, including what a render.yaml does and doesn't carry, is in [Render alternatives](https://runsite.app/blog/render-alternatives). The property the move does not change: the contract is with Render Services, Inc. in San Francisco, and [its Frankfurt region](https://render.com/docs/regions) operated by a US company is still operated by a US company.

### 2\. Fly.io — when the app has to be near the user

Fly.io runs containers in many regions and [prices every Machine per second](https://fly.io/docs/about/pricing/), and its proxy can [stop idle Machines and start them again on the next request](https://fly.io/docs/launch/autostop-autostart/). That makes it structurally different from a platform that rents you an always-on instance, though a stopped Machine is not free: its root filesystem is still billed. That fits an application whose latency budget is spent on the network rather than in the database — a chat backend, an API with users on three continents, anything where the round trip is the cost. It fits less well if what you wanted was fewer moving parts, because you are now reasoning about where each machine is and what happens when one region is unhealthy. The database story has changed shape. Fly now sells [Managed Postgres](https://fly.io/docs/mpg/), which lists version upgrades and migration tooling as still in development, and the older Fly Postgres is labelled [unmanaged, with no support offered for it](https://fly.io/docs/postgres/). Tables written a year ago tend to describe the second as if it were the first. Also a US operator: the terms are with Fly.io, Inc.

### 3\. Northflank — production controls, and the option to keep your own cloud account

Northflank sits a step further toward a real production platform than Railway aims to: pipelines, preview environments, more explicit control over how a release rolls out, and the option to [run the whole thing inside a cloud account you own](https://northflank.com/docs/v1/application/bring-your-own-cloud/use-other-cloud-providers-with-northflank) on AWS, Google Cloud or Azure, where Northflank manages a Kubernetes cluster for you. That last part is the reason it appears on shortlists that also contain much heavier things, because it lets a small team keep the commercial relationship with the infrastructure provider while outsourcing the control plane. The operator is [Northflank Ltd, company number 11918540](https://find-and-update.company-information.service.gov.uk/company/11918540), registered in London, which after Brexit means the United Kingdom rather than the EU. That matters to a procurement questionnaire even though the UK holds an adequacy decision, [renewed in December 2025 and due to expire on 27 December 2031](https://eur-lex.europa.eu/eli/dec%5Fimpl/2025/2574/oj/eng). A decision with an expiry date is worth understanding before you assume it settles the jurisdiction question. What adequacy is and how durable it is are covered in [Schrems II and EU data transfers](https://runsite.app/blog/schrems-ii-data-transfers).

### 4\. Coolify — the smallest invoice, if you own the server

Coolify is not a hosting company. It is an [open-source control plane under the Apache-2.0 licence](https://github.com/coollabsio/coolify) that you install on a machine you rent, and it gives you a git-push workflow, a dashboard and managed-looking databases on hardware you are administering yourself. On European infrastructure from Hetzner, Scaleway, OVHcloud, Exoscale or UpCloud, the monthly cost of running several services drops by an order of magnitude compared with any managed platform in this table. The exchange is that the operating system, the Docker disk, the reverse proxy, the certificates, the backups and the upgrades of Coolify itself become yours, and if it all runs on one box you have one failure domain. That exchange is priced out honestly, hour by hour, in [what self-hosting a PaaS actually costs](https://runsite.app/blog/coolify-vs-managed-paas).

### 5\. Dokploy — a lighter control plane on the same idea

Dokploy occupies the same slot as Coolify with a smaller surface: install it on a server, point it at a repository, get deployments and a UI. One licensing detail separates it. [Most of the repository is Apache-2.0](https://github.com/Dokploy/dokploy), but code in its proprietary folders is under a source-available licence that requires a commercial agreement for production use, so check which features you are relying on. [Dokku](https://github.com/dokku/dokku) and [Kamal](https://github.com/basecamp/kamal), both MIT-licensed, sit nearby, further toward the command line and further from a dashboard. Which of the four you pick matters much less than the decision they share, which is that you are taking on the box. If you are weighing this group at all, read the ledger for Coolify above and apply it here. The numbers are the same, and the deciding question is still how you value an evening spent on a certificate that did not renew.

### 6\. Clever Cloud — a French operator with a wide catalogue

Clever Cloud is a French company, [Clever Cloud SAS, registered in Nantes](https://clever.cloud/legal-notice/), running a managed platform with a broad service catalogue: application runtimes, Docker deployments, managed databases, object storage and observability. Compute is [billed by the second](https://www.clever.cloud/pricing/), so the cost tracks load rather than a fixed instance size; storage and traffic are billed by volume. It is the option to look at when you want the breadth of a large managed platform and an operating company inside the EU at the same time, which is a combination the ranked listicles rarely surface because they sort on feature count rather than on who signs the contract.

### 7\. Scalingo — when the operator has to survive a compliance review

Scalingo is French, managed, and the one on this list built most deliberately around being able to answer a procurement questionnaire: a European operating company ([Scalingo SAS, Strasbourg](https://scalingo.com/legal-notice)), European infrastructure underneath, and [ISO 27001:2022 and HDS certification](https://doc.scalingo.com/security/overview/compliance) for buyers who have to document where regulated data sits. Most products are [billed by the minute](https://doc.scalingo.com/platform/billing/process). If your blocker is not the price and not the feature set but a security review that keeps coming back with questions about subprocessors, this is the shape of answer that closes it. What such a review actually asks for is set out in [GDPR data residency](https://runsite.app/blog/gdpr-data-residency).

### 8\. Upsun — many environments per project

Upsun is the current product from [Platform.sh SAS, registered in Paris](https://upsun.com/trust-center/legal/impressum/), and its distinguishing idea is that an environment is cheap and clonable: by default [a new environment inherits the data and services of its parent](https://developer.upsun.com/docs/environments), databases included, which is a different mental model from a preview deployment that shares the production database. That suits a team with several people working on the same application at once and a real review process, and it is more machinery than a solo project needs. [Billing has four parts](https://developer.upsun.com/docs/administration/pricing): a monthly fee per project, CPU, memory and disk by usage, a licence fee per user, and paid add-ons. That reads differently from every other row in the table and is worth modelling before you commit.

### 9\. Runsite: EU-only infrastructure and a bill that does not move

Ours, and stated with its limits so you can discount it accordingly. Runsite is not a registered company. It is operated by an individual developer, running [web services on EU infrastructure](https://runsite.app/services/web-services) from €3/mo, with [managed PostgreSQL](https://runsite.app/services/postgresql) from €3/mo and [managed Redis](https://runsite.app/services/redis) from €5/mo alongside. You pick a fixed plan size and it is billed by the hour from a prepaid balance, rather than metered on the CPU and memory you actually touch. That is worse than Railway for an application that idles most of the day, better for one that does not, and predictable either way. There is one region, in Germany: against a platform advertising a dozen, that is a short list, and if you need to serve Singapore from Singapore it is the wrong answer. What the single region buys is that containers, variables, build artifacts and logs stay in the EU with no replication out. A signed [data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) comes with every account, and every subprocessor is named with its legal entity on the [compliance page](https://runsite.app/compliance), including the two outside the EEA. There is no import tool; the checklist further down is work you do yourself.

The column that actually decides it

Three of these nine are run by companies outside the EU, same as Railway. Two are software you operate yourself, three are EU companies, and one (ours) has no company behind it at all. That is not a criticism of any of them (Render and Fly.io are good at things Railway is not), but it does mean that moving from Railway to another US or UK operator changes your billing model and leaves the jurisdiction question exactly where it was, whichever region you deploy into. If nobody has asked you whose law applies to your customers’ data, that column is free to ignore. If somebody has, it is the only column that matters.

## Why people leave Railway

Three reasons recur, and they arrive at different stages. The first is that the bill moves. On top of a plan fee, Railway [charges for the RAM, CPU, egress and volume storage a service actually uses](https://docs.railway.com/pricing/plans): $10 per GB of RAM and $20 per vCPU per month, prorated. That is honest, and it also means a retry loop, a worker draining a queue that keeps refilling, or a job that was meant to run nightly can be billed as legitimate work and noticed at the end of the month. The second is the free allowance running out. New accounts get [a one-time $5 trial for up to 30 days](https://docs.railway.com/pricing/free-trial), after which the Free plan carries $1 of credit a month, and for people who have been around a while there is also the memory of a free tier that changed shape once already. The third arrives latest: a ceiling you meet while growing, whether that is the shape of the pricing at scale or the operational surface you want around a production service.

None of that is an indictment. Railway does something genuinely well, which is making an architecture legible as a graph rather than as a list of unrelated dashboard entries, and shortening the distance from a repository to a running application about as far as anyone has managed. The reasons above are reasons to leave, not evidence that staying is a mistake. If none of them describes your situation, the rest of this page is not trying to move you.

## What you're actually detaching

Every list of alternatives makes the same silent assumption: that your application is a container you can pick up and set down somewhere else. On Railway it usually is not. What you have is a project, and a project is a graph: services that know about each other, data that lives inside the same boundary as the code, and a build nobody wrote down. The four things below are what that graph costs to take apart. None of them appears in a feature matrix, and all of them surface in the first hour, not the last.

### The variables that are links, not values

Services on Railway can [reference each other's variables](https://docs.railway.com/variables/reference) with a `${{ Service.VARIABLE }}` template, so the connection string your application reads is a reference that gets resolved at deploy time rather than a literal you typed. This is a good feature and it is why the graph is convenient to build. It also means that exporting your environment gives you a page of pointers rather than a page of values, and that the resolution happens inside the platform you are leaving. The first symptom is an application that starts on the new host and cannot find its database, and the fix is to work out by hand what each reference was actually pointing at.

text

```
# What the graph looks like from inside the project
DATABASE_URL = ${{ Postgres.DATABASE_URL }}      <- resolved by the platform
REDIS_URL    = ${{ Redis.REDIS_URL }}            <- resolved by the platform
API_BASE     = ${{ api.RAILWAY_PUBLIC_DOMAIN }}  <- resolved by the platform

# What every other platform expects you to hand it
DATABASE_URL = postgresql://user:pass@host:5432/dbname
REDIS_URL    = redis://user:pass@host:6379
API_BASE     = https://api.example.com

# The gap between those two blocks is manual work,
# and it is discovered at the moment the app fails to boot.
```

### The databases that live inside the project

Postgres and Redis on Railway are [deployed from templates as services](https://docs.railway.com/databases/postgresql) sitting next to the application inside the same project, rather than as an external endpoint your application happens to point at. Railway's own docs are direct about what that means: [the database templates are unmanaged](https://docs.railway.com/databases), and backups, disaster recovery and tuning are yours. So moving them is the difference between changing a connection string and performing a migration. Leaving means getting the data out, finding it somewhere to live, and verifying that what came out goes back in, which for anything with real rows in it is the longest single task in the move. It is also the one people schedule last, because the application is the visible part and the database is not. If you are moving to [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql) or anywhere else, the restore is the step worth rehearsing before the day you need it.

### The build you never wrote

When there is no Dockerfile, Railway builds an image from your source with [Railpack](https://docs.railway.com/builds/railpack), which replaced Nixpacks, detecting the stack and assembling the image for you. [A Dockerfile in the repository is used instead](https://docs.railway.com/builds) if one exists. The convenience is real and the consequence is quiet: there may be no build definition in your repository, which means there is no artifact you own and no description of how the running thing was produced. Why that matters beyond this one migration is worked through in [how git push to deploy actually works](https://runsite.app/blog/git-push-to-deploy). The artifact is the part that makes a deploy reproducible, and a build that exists only inside a vendor's pipeline is a build you cannot take with you.

### The volume that doesn't come with you

If your service has attached storage, whatever is in it is not part of the repository and not part of the database dump. Uploaded files, generated assets, a SQLite file somebody added early and forgot about: these move only if you move them deliberately. Railway's volume backups will not do it for you, because [a backup can only be restored into the same project and environment](https://docs.railway.com/volumes/backups), and the docs describe no way to download one. [Each service gets a single volume](https://docs.railway.com/volumes/reference), which at least bounds the search. No comparison table has a row for this, and it is the item most likely to be discovered after the switchover instead of before. Moving that content to [object storage with no egress charges](https://runsite.app/services/s3-storage) is worth doing whether or not you change platforms, because it turns a machine-shaped dependency into an addressable one.

## The checklist to run before you pick anything

The useful order is the opposite of the obvious one. Rather than choosing a destination and then discovering what does not travel, reduce what ties you to the current platform first. Every item below can be done today, on Railway, without deciding anything about where you are going.

* **Write a Dockerfile and build the image locally.** If it builds on your machine and runs, you own the build. This single step converts a platform-detected application into a portable one, and it is the item that unblocks every other option including staying.
* **Flatten the variable graph into a plain file.** Go through every reference and write down the value it resolves to. Do it while the platform is still there to tell you, rather than at two in the morning while an application refuses to boot.
* **Take a database dump and restore it somewhere else.** Not just take it. Restore it, into a throwaway instance, and point something at it. A dump you have never restored is a hypothesis.
* **Move volume contents into object storage.** Anything written to attached disk is the least portable thing you own. Once files are addressed by URL, the service becomes stateless and the move becomes a deploy rather than an operation.
* **Deploy once from a clean clone.** Fresh checkout, no local state, no manual steps you have forgotten you perform. Whatever fails here is exactly what would have failed on the new platform, discovered on a day when nothing depends on it.

The point of doing this first

None of these five steps requires you to have chosen a destination, and none is wasted if you decide to stay. What they buy is optionality: after them, the difference between platforms is a comparison you can actually make, because the cost of being wrong has dropped from a weekend to an afternoon. Migrations get frightening in proportion to how much of the system lives somewhere you cannot inspect.

## Which of the nine, by the constraint that is actually binding

Once the project is portable, the nine sort themselves into three groups, and which group you want is decided by which constraint is actually binding. The mistake worth avoiding is picking by preference — the platform with the nicest dashboard, the one a friend mentioned — and discovering later that it does not address the thing that made you move in the first place.

| What is actually binding           | The group that addresses it           | What changes                                         | What does not                                              |
| ---------------------------------- | ------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------- |
| The bill is unpredictable          | Another managed platform, US-operated | Billing shape, interface, the region list            | Whose law governs your customers' data                     |
| The invoice is simply too large    | Run the platform layer yourself       | Monthly cost, and how much of the operations you own | That somebody has to be awake when it breaks               |
| A customer asked whose law applies | A European operating company          | The sentence you send to a security review           | The amount of migration work, which is the same either way |

Sorted by the constraint rather than by rank. The last column is the one that tends to go unread.

The first group is the lateral move, and it is the most common one. Render, Fly.io and the other US-operated managed platforms will change how you are billed and how the dashboard feels, and several of them are genuinely better than Railway at specific things. What that move does not touch is jurisdiction, since a US company operating a Frankfurt region remains a US company. That trade is examined in detail in [Render vs Railway](https://runsite.app/blog/render-vs-railway), including the case where the migration is motivated by something the migration does not change. One caveat if Vercel is on your shortlist: it is a different shape of platform rather than a peer of these, and what that costs a full-stack application is worked through in [Vercel alternatives for full-stack apps](https://runsite.app/blog/vercel-alternatives).

The second group is running the platform layer on infrastructure you rent, with Coolify, Dokploy, Dokku or Kamal on European hardware from Hetzner, Scaleway, OVHcloud, Exoscale or UpCloud. The invoice drops and the operating system, the Docker disk, the proxy and the backups become yours. That exchange is priced honestly in [what self-hosting a PaaS actually costs](https://runsite.app/blog/coolify-vs-managed-paas), which is the right page for the open-source and self-hosted end of this question.

The third group is European managed platforms, where the operating company is itself in the EU: Clever Cloud, Scalingo and Upsun among them. Runsite keeps data EU-only on the same terms but is operated by an individual rather than a company, which is a different answer to the entity question and worth knowing before you cite it in a questionnaire. This is the only group that changes the answer you give when a customer asks whose law governs their data, and it is also the group nobody puts in a ranked list, since the lists sort by feature count rather than by operator. What "European" can legitimately mean, and which of its meanings survives a procurement questionnaire, is set out in [choosing a European Heroku alternative](https://runsite.app/blog/european-heroku-alternative).

## What the move actually costs

With the checklist done, the move itself is smaller than it looks and lands in a predictable order. The application is the easy part: a Dockerfile you already wrote, a repository connection, a first deploy that either works or tells you what is missing. The variables need care but not much time. What takes real time is the data, and the schedule should be built backwards from it.

The failures worth planning for are the ones that come from the graph rather than from the new platform. A reference that resolved to something you never looked at. A service that talked to another over an internal address that no longer exists. A cron or worker that was configured in a dashboard rather than in the repository, and therefore does not exist in your clone at all. Each is small on its own, and each is the kind of thing that turns a two-hour job into an evening because it is invisible until something calls it.

Plan the move backwards from the database

Everything else can be redone in minutes if it goes wrong; the data cannot. Decide first how the dump is taken, how long the restore runs, and what happens to writes that arrive during it. If the answer to the last one is a short maintenance window, that is fine, as long as it is a decision you made and not one you discovered.

## How Runsite handles it

For the European group, concretely, and starting with what will not help you. There is no import tool. Runsite does not read your Railway project, does not convert a variable graph, and does not migrate a database for you; the checklist above is work you do yourself regardless of where you land. The second thing is pricing shape: Runsite charges fixed monthly plans rather than metering consumption, so an application that idles most of the day may well cost less on a consumption meter than it does here. [Web services on EU infrastructure](https://runsite.app/services/web-services) start at €3/mo for the entry Nano plan at 0.1 vCPU and 256 MB and run up to 8 vCPU and 16 GB on Business Plus.

The third limitation is regions: there is one, in Germany. Against a platform advertising a dozen, that is a short list, and if you need to serve Singapore from Singapore it is the wrong answer. What the single region buys is the sentence in the third row of the table above. Your containers, environment variables, build artifacts and logs stay inside the EU, there is no replication to non-EU regions, and a signed [GDPR data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) comes with every account. The processor named in it is an individual, not a company, and the [compliance page](https://runsite.app/compliance) says so right next to the subprocessor list.

The parts that make the checklist pay off are ordinary. Your Dockerfile is used when you supply one, and the stack is detected for Next.js, Django, Rails, FastAPI and others when you do not. Connect a repository on GitHub, GitLab or Bitbucket and a webhook builds on every push; releases are rolling behind a configurable health check, a failed check returns to the previous healthy version, and from the Standard plan (€12/mo) every pull request gets an environment on its own URL. A typical deploy takes around 30 seconds, and manual rollbacks with deploy history are on the Pro plan (€25/mo). Even on the entry Nano plan, active applications do not cold-start; a service sleeps only after 14 days idle. Alongside it sit [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql) from €3/mo and [managed Redis for caches and job queues](https://runsite.app/services/redis) from €5/mo, which is where the two services that currently live inside your Railway project would go. Setup details are in the [Runsite docs](https://docs.runsite.app).

## The short version

The ranked lists are answering a question you do not have. What decides how hard leaving Railway is has nothing to do with which platform tops which table, and everything to do with four things inside your current project: variables that are references rather than values, databases that live inside the project boundary, a build that was produced without a Dockerfile, and whatever is sitting on an attached volume. Take those apart first, in that order, and the destination becomes a choice instead of a commitment.

Then pick the group by the constraint that is actually binding. Another US-operated platform if the billing model is the problem, your own server if the invoice is, a European operating company if somebody has started asking whose law applies to your customers' data. The last one is the only question the other two cannot answer, and it is far cheaper to settle before a migration than after. Deploying one service on [a European platform that builds from your own Dockerfile](https://runsite.app/services/web-services) is a short way to find out whether the checklist actually made you portable.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Which is better, Railway or Render?

Neither is better in general; they fail in different directions. Railway meters what you actually consume, which is honest and means a retry loop or a worker draining a queue can be billed as legitimate work and noticed at the end of the month. Render sells fixed instance tiers, so the invoice stops moving but you pay for capacity an idle app is not using. If your complaint about Railway is that the bill surprised you, Render addresses it directly. If your complaint is that the platform ran out of room as you grew, both platforms sit in roughly the same class and the move will not fix it. And if the reason you are looking is that somebody asked whose law applies to your customers' data, the answer is unchanged: Railway Corporation and Render Services, Inc. are both based in San Francisco, and a Frankfurt region operated by a US company remains operated by a US company. The full comparison, including free tiers and regions, is in the Runsite article Render vs Railway.

### What is the best European alternative to Railway?

Three platforms in common use are operated by companies established in the EU: Clever Cloud (France), Scalingo (France), and Upsun, which is Platform.sh SAS in Paris. Which is best depends on what you are optimising for. Clever Cloud has the broadest managed-service catalogue of the three. Scalingo is built most deliberately around surviving a compliance review. Upsun suits teams that want a full clonable environment per branch. Runsite, which publishes this page, bills fixed monthly plans rather than metering consumption and runs a single region in Germany; it is operated by an individual developer rather than a registered company, so if the question is specifically which EU legal entity you are contracting with, the three above answer it and this one does not. Note that Northflank, which appears on many European shortlists, is registered in London. That is the United Kingdom rather than the EU, which is a different answer to give a procurement questionnaire even under the UK adequacy decision the Commission renewed in December 2025, valid until 27 December 2031.

### Is there a free alternative to Railway?

Several platforms offer a free tier, but the useful question is not whether one exists. What matters is what that tier does to an idle application. Most of them stop the container after a period without traffic, which means the next visitor waits for it to start again, and that visitor is usually the one you cared about: someone following a demo link, or a webhook that fires twice a week. The mechanics of that wait are covered in the Runsite article on cold starts and why free-tier apps fall asleep. Two other things are worth checking on any free plan. What happens when the monthly allowance runs out, since the service may suspend instead of degrading, and what happens to the free database, which on most platforms expires well before the free application does. For comparison, Railway's own trial is a one-time $5 for up to 30 days, followed by $1 of monthly credit on its Free plan. Runsite has no free web tier: the entry Nano plan runs one web service at 0.1 vCPU and 256 MB for €3/mo, covered by the €5 starting credit, active applications do not cold-start, and a service sleeps only after 14 days idle.

### What is the best open-source alternative to Railway?

The open-source options are self-hosted control planes, not hosted platforms, which makes them a different kind of thing rather than a cheaper version of the same thing. Coolify is the most widely used, with Dokploy, Dokku and Kamal covering nearby ground, and all of them run on infrastructure you rent, which in Europe usually means Hetzner, Scaleway, OVHcloud, Exoscale or UpCloud. What you gain is a much smaller invoice and full control of the box. What you take on is the operating system, the Docker disk, the reverse proxy, the certificates, the backups and the upgrades of the control plane itself, plus a single failure domain if everything runs on one server. Whether that exchange is worth it depends on how you value your own time, and it is priced out in full in the Runsite comparison of Coolify against a managed platform.

### How do I migrate off Railway?

Do the portability work before you choose a destination, because all of it is useful regardless of where you end up. Write a Dockerfile and confirm the image builds and runs locally, which turns a platform-detected application into one you own. Flatten the environment: Railway services can reference each other's variables through \`${{ Service.VARIABLE }}\` templates, so what you have is a graph of pointers rather than a list of values, and each one needs resolving into a literal while the platform is still there to tell you what it meant. Take a database dump and actually restore it somewhere, since an untested dump is a hypothesis rather than a backup. Move anything on an attached volume into object storage, which is the least portable thing you own. Then deploy once from a clean clone to find the manual steps you have forgotten you perform. After that the move itself is mostly the database, and the schedule should be planned backwards from how long the restore takes.

### Is there a European alternative to Railway?

Yes, and it is a different question from whether a platform offers a European region. Clever Cloud, Scalingo and Upsun are examples of platforms whose operating company is itself established in the EU, which is the property that determines whose law applies to your customers' data. Runsite, which publishes this page, runs EU-only infrastructure but is operated by an individual rather than a registered company, so it is not an example of that particular property. Railway Corporation is a US company based in San Francisco, and so are most of the platforms that appear in alternatives listicles, which means moving between them changes your billing model and leaves the jurisdiction question exactly where it was, whichever region you deploy into. Two checks separate the two situations: which legal entity appears in the terms you are agreeing to and where it is registered, and who the subprocessors are and where they sit. Both live in the terms or the DPA, not on the pricing page. The fuller version of that test, including the three different things "European" is used to mean, is in the Runsite guide to choosing a European Heroku alternative.

Keep reading

---

# 10 Vercel Alternatives in 2026: Full-Stack and EU-Hosted

> Netlify, Cloudflare Pages, Render, Railway, Fly.io, Northflank, Coolify, Clever Cloud, Scalingo and Runsite — which run a live process, and who operates each.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. 10 Vercel Alternatives in 2026 for Full-Stack Apps, Not Just the Frontend

DeploymentAugust 14, 202615 min read

# 10 Vercel Alternatives in 2026 for Full-Stack Apps, Not Just the Frontend

Ten platforms that replace Vercel, compared on the thing that decides it: whether each one runs a long-lived process, and who operates it.

## The short version

Vercel hosts build output and functions, and is good at it. A list of alternatives never resolves because the names on it are four different shapes of hosting. Count what your application actually has first: a process that must stay alive, state, scheduled work, files that outlast a deploy. Several of those mean a container.

[Teo Marquardt](https://runsite.app/about#author)· Updated September 26, 2026

The short version. If you deploy build output and nothing else, Cloudflare Pages and Netlify are the closest swaps and the cheapest. If your application has a server in it — a process that stays alive, a database, a queue, a nightly job — then Netlify and Cloudflare Pages are the same shape as Vercel and will leave you where you are; the platforms that change something are Render, Railway, Fly.io and Northflank. If the invoice is the problem, Coolify on European hardware. If somebody has asked whose law governs your users’ data, the three with an EU operating company are Clever Cloud, Scalingo and Upsun — Runsite, which publishes this page, keeps the data in the EU but is not itself an incorporated company. All ten are described below, with the property that separates them.

Open three lists of Vercel alternatives and the same names appear in a different order: Netlify, Cloudflare Pages, Render, Railway, Fly.io, and a self-hosted control plane on a rented box. The ranking is the part that does not work. Those are not competing products. They are four different shapes of hosting, and a table that sorts them by feature count is holding a CDN up against a container scheduler and declaring a winner.

Which is why the lists cannot help you, and why the answer changes completely depending on something none of them asks. A site that compiles to files and a Next.js application with a database, a queue and a nightly job are two unrelated hosting problems that happen to share a starting point. Until you have said which one you have, a ranked list is answering a question nobody asked.

Two adjacent questions are out of scope here. If you are leaving a platform and want to know what has to be unpicked before you can move at all, that mechanic is worked through in [what you are actually detaching from Railway](https://runsite.app/blog/railway-alternatives). If the real question is whether to run the platform layer on a server you administer, the ledger for that is in [self-hosted PaaS versus managed](https://runsite.app/blog/coolify-vs-managed-paas). What follows is about the shape of your application and the shape of the thing that should be running it.

## Why the swap question is shaped wrong

Vercel deploys the output of your build and a set of serverless functions, and as of writing that is the boundary of what it runs. It is very good inside that boundary. What it does not include, by design rather than by omission, is a process that stays alive between requests, a database, a queue, a scheduler, or a filesystem that survives the next deploy. Those are all real requirements of an ordinary application, and on Vercel each one is satisfied by something you buy separately and connect over the network.

Follow that through and the consequence is structural. An application that started as a frontend and grew a backend is, by the time it needs alternatives, distributed across a hosting platform, a database vendor, possibly a cache, an object store for anything users upload, and a scheduler for the jobs that have to run whether or not a request arrives. Nobody chose that architecture in a meeting. It accumulated, one integration at a time, because the platform underneath supplied the first piece and not the rest.

You are not moving one thing

This is why "what should I use instead of Vercel" resists a one-line answer. The thing you are moving is not a deployment. It is a set of decisions about where each part of your application lives, several of which were made implicitly, and a replacement platform will absorb some of them and leave others exactly where they are. The list of alternatives is the last step of that decision, not the first.

## Four things a full-stack app needs that a frontend platform doesn't have

A platform that deploys build output and functions leaves exactly four gaps open: a process that stays alive between requests, state that has to live somewhere, work that runs on a schedule, and files that outlast a deploy. None of them is a defect. They follow from what that kind of platform is for, and every one of them is already a line item you pay for somewhere else, or a workaround you have already built.

### A process that stays alive

A serverless function has a ceiling on how long it may run, and as of writing the exact ceiling varies by plan and has changed more than once, so the number matters less than the fact that one exists. Anything that wants to outlive a request runs into it: a WebSocket connection held open for a collaborative editor, a server-sent event stream, an import that takes four minutes on a large file, a worker that drains a queue continuously rather than in bursts. The usual workarounds are to split the work into chunks that each fit under the limit, or to move that one piece onto a second platform that runs containers, which is how a stack ends up with two hosting providers. The related tax on the same seam, an instance that has to be started before it can answer, is covered in [why free-tier apps fall asleep](https://runsite.app/blog/cold-starts).

### State that lives somewhere

No database comes with the box, so you attach one, and the attachment has a property that surprises people the first time they meet it. A function is an instance that appears for one invocation, which means a naive database client opens a connection per invocation, and a burst of traffic turns into a burst of connections against a server that has a hard limit on how many it will hold. This is not the database misbehaving. It is a runtime with no long-lived process meeting a protocol that assumes one, and the fix is a pooler in between, which is a component you now operate or pay for. What the pooler is actually doing, and why the two ports matter, is in [connection pooling with PgBouncer](https://runsite.app/blog/pgbouncer-connection-pooling). A platform that runs a container sidesteps the problem rather than solving it, because the process is already there and holds its own pool, and [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql) is then an endpoint rather than an architecture.

### Scheduled and background work

Every application acquires work that is not triggered by a visitor: expiring sessions, sending the digest, reconciling a payment provider, retrying whatever failed overnight. On a frontend platform these live as an add-on, usually a scheduler that calls one of your functions on a timer, which works and inherits the execution ceiling from the section above. On a platform that runs containers they are ordinary services, and a queue consumer is simply a process that is always running. If you have been keeping a job alive by having an external service ping a URL every five minutes, that is the workaround this gap produces, and [managed cron jobs that run as a first-class service](https://runsite.app/services/cron-jobs) alongside [managed Redis for caches and job queues](https://runsite.app/services/redis) is what replacing it looks like.

### Files that survive a deploy

There is no writable disk, or more precisely there is one that disappears with the instance, so anything a user uploads has to go somewhere addressable from the moment it arrives. Why that is true of nearly every modern deployment target, not only this one, is explained in [how git push to deploy actually works](https://runsite.app/blog/git-push-to-deploy). The practical consequence is an object store in the stack from day one, and the thing to check on it is not capacity but transfer, since serving user media is mostly egress. On Runsite that line is [object storage with no egress charges](https://runsite.app/services/s3-storage); elsewhere it is metered, and metered transfer is the part of a storage bill that grows with your traffic rather than with your data.

## The bill is a different shape than the pricing page

Cost is the reason most people start reading about alternatives, and the pricing page is a poor guide to it, because the parts that move are not the parts printed in large type. Three things are metered rather than fixed as of writing: outbound bandwidth, function execution, and the number of people on the team. A plan price plus three meters is not a monthly cost, it is a monthly estimate, and the months where it is wrong are the months where something went right and traffic arrived. Worth checking separately, because it catches people at the worst moment: as of writing the free tier carries a restriction on commercial use, which is set out on [Vercel's own pricing page](https://vercel.com/pricing) and is the kind of clause that surfaces when a side project starts taking money.

The second half of the arithmetic is the one nobody puts in a comparison table. Because the platform supplies one piece of the application, the other pieces have their own invoices, and each of those is metered on its own terms. The database bills for storage and often for compute time. The object store bills for what it stores and, on most providers, again for every byte a visitor downloads, which is the mechanic taken apart in [why downloading your own data costs so much](https://runsite.app/blog/s3-egress-fees). Add the scheduler, add the observability tool that replaced the logs you used to have. The total cost of a Vercel-shaped stack cannot be read off Vercel's pricing page in principle, which is why the number people quote from memory is usually the smallest of the several they pay.

## Which of the ten, picked by what you are deploying

With the gaps and the meters named, the candidates sort themselves into four shapes, and the question that picks one is what you are deploying rather than which platform ranks highest. The table below is deliberately built on the last column, which is the one that decides whether a migration was worth doing.

| What you are deploying                                 | The shape that fits                           | What it changes                                             | What it does not                                            |
| ------------------------------------------------------ | --------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| Build output only, no server at request time           | A static host with a CDN                      | Bandwidth model, build pipeline, the amount you operate     | Anything that needs to run when a request arrives           |
| A server that has to be running                        | A container platform                          | Execution ceilings, connections, schedules, background work | That the database and the object store still live somewhere |
| The same stack, and a customer asked whose law applies | A European operating company                  | The sentence you send to a security review                  | The amount of migration work, which is the same either way  |
| The same stack, and the invoice is the problem         | The platform layer on infrastructure you rent | Monthly cost, and how much of the operations you own        | That somebody has to be awake when it breaks                |

Sorted by the shape of the application rather than by rank. The final column is where migrations disappoint people.

The first row is the honest answer more often than people expect. If what you deploy is the output of a build, a static host with a CDN is a smaller and cheaper thing than a platform with a functions runtime attached, and [static site hosting with a global CDN and EU-hosted builds](https://runsite.app/services/static-sites) does the job for Astro, Hugo, Vite, SvelteKit and a Next.js static export. The lateral move belongs here too, and it should be named plainly: Netlify and Cloudflare Pages are the same shape of platform as Vercel, so moving between them changes the interface, the metering and the ecosystem, and leaves all four gaps from the previous section exactly where they were.

The second row is what most people who are unhappy with a frontend platform actually want. A container runs your process, holds its own connection pool, runs a worker next to the web server, and has no execution ceiling to design around. [A European container platform that runs a long-lived process rather than a function](https://runsite.app/services/web-services) is the same category as Render, Railway and Fly.io, and the comparison between two of those, including what their two billing models each get wrong, is in [Render vs Railway](https://runsite.app/blog/render-vs-railway).

The third and fourth rows describe the same application arriving from a different direction. If what pushed you was a question about jurisdiction, the group that answers it is platforms whose operating company is established in the EU: Clever Cloud, Scalingo and Upsun among them, and what "European" can legitimately mean is set out in [choosing a European Heroku alternative](https://runsite.app/blog/european-heroku-alternative). If the constraint is the invoice, the answer is running Coolify, Dokploy, Dokku or Kamal on European hardware from Hetzner, Scaleway, OVHcloud, Exoscale or UpCloud, and what that hands you along with the saving is priced in [what self-hosting a PaaS actually costs](https://runsite.app/blog/coolify-vs-managed-paas).

## 10 Vercel alternatives in 2026, and what each one actually replaces

Here they are by name, with the two columns that decide the outcome. The first is whether the platform runs a long-lived process, because that single property is what separates a Vercel replacement from a Vercel repaint — four of the ten do not, and moving to one of those leaves every gap from the previous section exactly where it was. The second is where the operating company is registered, which is what determines whose law reaches your users’ data, regardless of which region you pick from a dropdown.

| Platform              | Runs a long-lived process?      | Operator registered in                              | Best for                                                      |
| --------------------- | ------------------------------- | --------------------------------------------------- | ------------------------------------------------------------- |
| Netlify               | No — build output and functions | United States                                       | A lateral move with a familiar workflow                       |
| Cloudflare Pages      | No — static plus Workers        | United States                                       | Static sites, and the cheapest bandwidth of the ten           |
| Render                | Yes                             | United States                                       | The frontend platform swapped for a real container platform   |
| Railway               | Yes                             | United States                                       | Full-stack projects where the services reference each other   |
| Fly.io                | Yes                             | United States                                       | Apps that must run close to users in several regions          |
| Northflank            | Yes                             | United Kingdom                                      | Production controls, or running in your own cloud account     |
| Coolify (self-hosted) | Yes — on your server            | Open source — you rent the box                      | The smallest invoice, if you own the operations               |
| Clever Cloud          | Yes                             | France                                              | A French operator with a broad managed catalogue              |
| Scalingo              | Yes                             | France                                              | Workloads that have to survive a compliance review            |
| Runsite               | Yes                             | Not incorporated — sole operator, EU infrastructure | EU-only data, a fixed bill, and no cold starts on active apps |

Ten alternatives to Vercel, as of writing. The second column is the one that decides whether a migration solves anything.

### 1\. Netlify — the closest swap, and the one that changes least

Netlify is the same shape of platform as Vercel: a git-based workflow, deploy previews, build output served from a CDN, and functions for the parts that need to execute. Moving is close to frictionless, which is exactly the warning. The interface changes, the metering changes and the ecosystem changes; the four gaps stay open, because a platform built on the same model has the same boundary. It is the right answer if your complaint is commercial — pricing, a plan restriction, a support experience — and the wrong one if your complaint is that your application outgrew the model. If you are leaving Netlify itself and only ship static files, the European options are compared in [European Netlify alternatives for static sites](https://runsite.app/blog/netlify-alternatives-europe).

### 2\. Cloudflare Pages — the cheapest, if you really are static

For a site that compiles to files, Cloudflare Pages is hard to beat on cost, and its bandwidth model is the reason it wins almost every listicle. Workers cover the parts that need to run, within the constraints of a runtime that is not Node and does not hold a connection open across requests. That last detail is where projects get caught: a Next.js app with a database behaves differently on a Workers runtime than on a server, and finding out mid-migration is expensive. If your output really is files — Astro, Hugo, Vite, SvelteKit, a Next.js static export — this or [static hosting with an EU-built CDN](https://runsite.app/services/static-sites) is a smaller and cheaper thing than any platform in this table.

### 3\. Render — the frontend platform swapped for a container platform

This is the row most people leaving Vercel actually want. Render runs a container, which means the process stays alive between requests, holds its own database connection pool, has no execution ceiling to design around, and can run a background worker beside the web server. Managed Postgres and a Redis-compatible store sit next to it, so three of the four gaps close at once. Its free tier stops an idle service, which matters for demo links — see [why free-tier apps fall asleep](https://runsite.app/blog/cold-starts). US operator.

### 4\. Railway — when the app is several services that know about each other

Railway makes an architecture legible as a graph rather than a list of unrelated dashboard entries, and it provisions Postgres and Redis inside the same project boundary as the code. For an application that is a web service plus a worker plus two datastores, that is a genuinely better mental model than assembling the same thing from four vendors. Billing is metered against actual consumption, which is honest and occasionally surprising. What that graph costs to take apart later is the subject of [Railway alternatives](https://runsite.app/blog/railway-alternatives), and the head-to-head with Render is in [Render vs Railway](https://runsite.app/blog/render-vs-railway). US operator.

### 5\. Fly.io — when the latency budget is spent on the network

Fly.io runs containers in many regions and bills per second, with machines that can scale to zero between requests. If you left Vercel because you liked being close to users but needed a real server, this is the shape that keeps both. The cost is that you are now reasoning about where each machine lives and what happens when one region is unhealthy, which is more operational thinking than a single-region platform asks of you. Its managed database offering has changed more than once, so check what is actually managed on the day you look. US operator.

### 6\. Northflank — production controls, or your own cloud account

Northflank aims further toward a production platform than Vercel does: pipelines, preview environments, explicit control over how a release rolls out, and the option to run everything inside a cloud account you own rather than the vendor’s. Worth knowing before you file it under "European": the operator is Northflank Ltd, registered in London, which since Brexit means the United Kingdom rather than the EU. The UK holds an adequacy decision at the time of writing, which is not the same as being inside the union, and the difference is one a procurement questionnaire will notice — see [Schrems II and EU data transfers](https://runsite.app/blog/schrems-ii-data-transfers) for why adequacy is a decision that can be revisited.

### 7\. Coolify — the open-source answer, and the smallest invoice

Of everything in this table, Coolify is the one people arrive at straight from a Vercel invoice, because the arithmetic is startling: the four gaps close at once on a single rented machine, and the monthly figure is the price of the machine. It is not a hosting company — it is software you install and then run, and the dashboard’s resemblance to a managed platform hides the fact that nobody else is watching it. Dokploy, Dokku, Kamal and CapRover cover the same idea with less UI. The honest version of the arithmetic, including the hours nobody bills you for, is in [what self-hosting a PaaS actually costs](https://runsite.app/blog/coolify-vs-managed-paas).

### 8\. Clever Cloud — a French operator with a wide catalogue

The relevant thing about Clever Cloud from a Vercel seat is that it closes all four gaps under one operator: a runtime that keeps your process alive, databases, an object store for uploads, and somewhere for scheduled work to run. That collapses the vendor sprawl described at the top of this page back into a single bill and a single support desk. It is French, which also settles the jurisdiction column, and it bills per second, so an application with quiet nights is not charged as though it were busy. Where it costs you is familiarity: the deploy flow is not modelled on Vercel and the first week will feel like relearning something you already knew.

### 9\. Scalingo — built to survive a compliance review

Reach for Scalingo when the thing blocking you is not the architecture but a document. It is a French operator with French infrastructure and the certifications a regulated buyer expects to see named, which is what a stalled security review is usually waiting for. Upsun, Platform.sh SAS in Paris, sits beside it with a different emphasis: every branch can get a full environment with its own copy of the data, which is a stronger version of the preview deployments you are used to and a heavier machine to run. Neither is the cheapest row here, and neither is trying to be. What a review of this kind actually asks for is listed in [GDPR data residency](https://runsite.app/blog/gdpr-data-residency).

### 10\. Runsite — an EU operator, a fixed bill, no cold starts on active apps

Ours, described with the parts that will not suit you. Runsite runs a container rather than a function, which is the whole difference this page is about: the process survives between requests, holds its own connection pool, and has no execution ceiling to design a job around. [Web services](https://runsite.app/services/web-services), [PostgreSQL](https://runsite.app/services/postgresql), [Redis](https://runsite.app/services/redis) and [object storage](https://runsite.app/services/s3-storage) each have an entry plan of €5/mo or less, so the four gaps close inside one invoice instead of four. It is operated by an individual developer rather than a registered company, so the jurisdiction column reads differently from Scalingo SAS or Clever Cloud SAS, and there is exactly one region, in Germany — if latency to users outside Europe is what you liked about Vercel, that is a real loss and you should weigh it. Plans are a flat monthly figure, so a quiet month costs the same as a busy one, which is a downgrade if your traffic is genuinely spiky. Active apps do not cold-start even on the entry Nano plan, and every plan ships a signed [data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa).

Six of the ten are operated from outside the EU

None of that is a mark against them — Cloudflare’s bandwidth economics and Render’s simplicity are each better than what the EU rows offer. It does mean that a migration between any two of those six is a change of dashboard and pricing model with the legal position untouched, no matter which region you select at signup. Ignore the column outright if nobody has raised it. The moment somebody does, it is the only column in the table capable of answering them.

## The Next.js question

For a large share of the people reading this, the worry underneath the search is not hosting in general but Next.js hosting in particular. The framework is developed by Vercel, and the reasonable version of the concern is not about licensing, which is permissive, but about the features whose behaviour depends on what is running underneath them. As of writing, the ones that come up are:

* **Incremental static regeneration**, which needs somewhere to hold and invalidate the regenerated pages.
* **Image optimisation**, which needs a process to transform images on demand and a cache to keep the results.
* **Middleware on the edge runtime**, which runs in a restricted environment rather than in a normal Node process.
* **Streaming and partial rendering**, which need the connection held open while the response is assembled.

None of these is proprietary, and the framework's own [self-hosting documentation](https://nextjs.org/docs/app/getting-started/deploying) covers how each is supported outside Vercel, which is the source worth reading rather than any vendor's summary of it. What decides the outcome is the hosting shape from the table above. A static export removes the server entirely, and with it everything on that list that needs one, which simplifies a content site enormously and breaks an application. A container running the framework's own server keeps most of the list, because the implementation ships with the framework; the edge runtime is the exception, having been designed around infrastructure a container does not have. Which of the two your project wants is a decision in its own right and a subject for another article.

What ties you is the implementation, not the framework

Framework lock-in is the wrong thing to worry about here; the code travels. What travels less well is the set of features whose implementation was supplied by the platform, since those are the ones that quietly stop working, or start costing money you were not paying, on a host that supplies them differently. Check that list against your own application before you compare anything, and you will know which rows of the table are even open to you.

## Vercel alternatives in Europe: where the operator is registered

If you searched for a Vercel alternative in Europe, the table above gives you three names. Clever Cloud and Scalingo are both operated by French companies on French infrastructure, and both run a long-lived process. Runsite runs one too, on infrastructure in Germany, with the caveat that it has no incorporated company behind it yet. A fourth option isn't on the list because it isn't a vendor: Coolify or Dokploy on a server rented from Hetzner, Scaleway or OVHcloud, and you are the one operating the platform.

One question survives every comparison in this article, and the lists almost never contain it. Vercel Inc. is a US company as of writing, which is stated in its own terms, and so are Netlify, Cloudflare, Render, Railway and Fly.io. Choosing a region does not change that. A region determines where the bytes are written, which is worth requiring and helps with latency, and it says nothing about which government can compel the company operating the service. Moving between US platforms is a change of billing model and interface with the jurisdiction question left exactly where it was.

If nobody has asked you whose law applies to your customers' data, this row of the table is not binding and you can skip it. If somebody has, or if you sell to anyone with a procurement process, the property that answers it is where the operating company is established, not where the region is. The three different things the word "European" gets used to mean, and which of them survives a security questionnaire, are set out in [choosing a European Heroku alternative](https://runsite.app/blog/european-heroku-alternative). Settling it while you are still choosing costs you an afternoon of reading; settling it afterwards costs you the migration twice.

## How Runsite handles it

Starting with what does not exist. There is no import tool: Runsite does not read a Vercel project, does not convert a configuration file, and does not ship a Next.js adapter of its own. There is one region, in Germany, so if you need to serve Sydney from Sydney this is the wrong answer. Static assets are cached at edge locations, and that is a cache rather than an execution environment, which means code running at the edge is not something Runsite offers and middleware written for that runtime is not a thing to plan around here.

What it does have is the two shapes from the top of the table under one account. [Static site hosting with EU-hosted builds](https://runsite.app/services/static-sites) covers a build that produces files, with unlimited sites and 100 GB of bandwidth a month on the free tier, automatic SSL on your own domain, and a preview URL per pull request. [Web services on EU infrastructure](https://runsite.app/services/web-services) cover the case where a process has to be running, from a free tier of one service at 0.1 vCPU and 256 MB up to 8 vCPU and 16 GB, with your Dockerfile used when you supply one and the stack detected for Next.js, Django, Rails, FastAPI and others when you do not. Deploys are rolling behind a health check with automatic rollback, a choice explained in [blue-green versus rolling deployments](https://runsite.app/blog/blue-green-vs-rolling-deployments), and active applications do not cold-start even on the entry Nano plan; a service there sleeps only after 14 days idle.

On the bill, the shape is the opposite of a meter, and the trade goes both ways. Plans are fixed monthly prices as of writing, from €3 to €180, so an application that idles for twenty hours a day may genuinely cost less on a consumption meter than it does here; the thing a fixed plan buys is knowing the number in advance. The pieces from the four gaps sit alongside it rather than at other vendors: [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql) from €3/mo and [managed Redis](https://runsite.app/services/redis) from €5/mo, [managed cron jobs](https://runsite.app/services/cron-jobs), and [object storage with no egress charges](https://runsite.app/services/s3-storage) at €0.025 per GB per month after 5 GB free. All of it runs in the one region, there is no replication to non-EU regions, and a signed [GDPR data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) comes with every account. Setup details are in the [Runsite docs](https://docs.runsite.app).

## The short version

Vercel is a platform for build output and functions, and it is good at that. The reason a list of alternatives never resolves is that the names on it are four different shapes of hosting, and which shape you need is decided by four things your application either has or does not: a process that must stay alive, state that has to live somewhere, work that runs on a schedule, and files that outlast a deploy. Count how many of those you have before you compare anything. If the answer is none, you want a static host and you have been overpaying for a functions runtime you do not use. If the answer is most of them, you want a container.

Then check the two things that no feature matrix contains: which Next.js features you depend on and where their implementation comes from, and which legal entity is named in the terms you are agreeing to. Those decide whether a migration solves your problem or relocates it. If it turns out you want the second row of the table, [a European platform that runs your container from your own Dockerfile](https://runsite.app/services/web-services) has a free tier, and one service deployed onto it will tell you more in an afternoon than another ranked list will.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is better than Vercel?

Nothing is better in the abstract, because the platforms usually offered as answers are not the same kind of product. Vercel deploys build output and serverless functions. A static host with a CDN does less than that and costs less, and if your project compiles to files it is the better fit. A container platform such as Render, Railway, Fly.io or Runsite does something different: it runs a process that stays alive, which is what you want if your application holds WebSocket connections, streams responses, drains a queue, or runs a job longer than a function is allowed to run. So the useful question is what you deploy. If it is a frontend with a few API routes, Vercel is a short path and moving may not gain you much. If it is an application with a database, background work and files that have to persist, a container is the shape that removes the workarounds rather than rehousing them.

### What are some platforms similar to Vercel?

The closest in shape are Netlify and Cloudflare Pages, which also deploy build output with a functions runtime attached, and that similarity is the catch. If you are looking for alternatives because your application outgrew what a frontend platform runs, then a platform of the same shape will reproduce the same limits with a different dashboard. What differs between them is metering, the free tier, build behaviour and ecosystem, which matters if those are your reasons for moving. If the reason is the shape, the comparable products are container platforms rather than frontend platforms, and among them the choice is mostly about billing model and where the operating company is registered. Both of those, held against each other on two named platforms, are in the Runsite comparison of Render and Railway.

### What is cheaper than Vercel?

Almost anything can be, and almost anything can also be more expensive, because the two models fail in opposite directions. Metered billing charges for bandwidth, function execution and seats, so it is cheap when nothing is happening and unbounded when something is. A fixed monthly plan is the reverse: predictable, and wasteful for an application that idles most of the day. Neither is cheaper as a fact about the platform; it depends on your traffic shape, and the honest way to compare is to take last month's actual numbers rather than a projection. The part that gets left out of both estimates is that a frontend platform hosts one piece of your stack, so the database, the object store, the scheduler and the observability tool each arrive with their own bill, and on most object stores the bill grows every time a visitor downloads a file. Runsite charges fixed plans from €3/mo as of writing and does not meter egress on its object storage, which makes the total predictable rather than automatically smaller.

### What's better, Vercel or Netlify?

They are close enough that the answer usually comes down to preference, which is a hint that the comparison may be the wrong one. Both deploy build output with a serverless functions runtime beside it, both build from Git with preview deployments per pull request, and both meter bandwidth and function usage. Vercel has the tighter relationship with Next.js, since it develops the framework; Netlify is more neutral across generators and has its own set of build plugins. What neither of them changes is the shape: no long-lived process, no database included, no persistent disk, and scheduled work as an add-on. If those are what pushed you into comparing hosts, choosing between these two rearranges the problem rather than removing it, and the platforms that do remove it are the ones that run containers. Both companies are also registered in the US as of writing, so on the question of whose law governs your customers' data they give the same answer.

Keep reading

---

# Blue-Green vs Rolling Deployments: What Zero Downtime Costs

> Blue-green and rolling deploys promise zero downtime. What differs is how many versions hit your database at once, and what your platform lets you pick.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Blue-Green vs Rolling Deployments: What Zero Downtime Costs You

DeploymentAugust 17, 202611 min read

# Blue-Green vs Rolling Deployments: What Zero Downtime Costs You

Both strategies promise your users never see the release. The difference that decides between them is how many versions of your code are talking to one database at the same time.

## The short version

Both promise users never see the release. The difference is how many versions of your code talk to one database at once. Rolling replaces instances in waves, so old and new run together and every migration must be backward compatible. Blue-green boots a full second environment and pays that capacity cost once.

[Teo Marquardt](https://runsite.app/about#author)

Both strategies are sold with the same sentence: your users never see the release. Both deliver on it most of the time, and that sentence is where the useful part of the comparison ends. Downtime is not what separates them. What separates them is what your application is doing during the minutes when two versions of it are alive, and what your data is exposed to while that lasts.

Search the comparison and you get a stable set of answers. Blue-green rolls back faster. Rolling costs less infrastructure. Canary is safer for a risky change. All true, and all of it about the code, which is the easy half. The half that decides is stateful, and most of the write-ups stop at the container boundary before they reach it.

How code gets from a push to a running container is a separate mechanic, worked through in [git push to deploy](https://runsite.app/blog/git-push-to-deploy), which stops deliberately at the moment traffic starts moving. This picks up at that moment.

## Zero-downtime deployment is the promise both of them make

A blue-green release runs two complete copies of your application. One is live and serving everyone; the other holds the new version and serves nobody. When the new copy looks healthy, a router or load balancer moves all traffic across in one movement. The old copy stays running for a while afterwards, and that is what makes the rollback fast: you move the traffic back.

A rolling release has one copy and replaces it from the inside. Instances of the new version start a few at a time, each one is checked, and each one that passes takes over from an old instance, which then retires. There is no second environment and no single switch. The release is a sequence of small swaps that ends when the last old instance is gone.

A lot of the confusion in this comparison is vocabulary, so it pays to pin the names down. Blue-green, blue/green and blue–green are the same thing. Red/black is the same thing again, under the name Netflix used for it. A rolling update in Kubernetes vocabulary is a rolling deployment in everyone else's. The original write-up of the pattern is [Martin Fowler's, from 2010](https://martinfowler.com/bliki/BlueGreenDeployment.html), and it is still the description most of the current articles are paraphrasing.

|                                 | Blue-green                                                    | Rolling                                                          |
| ------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------- |
| What runs during the release    | Two complete copies of the application, one live and one idle | One copy, with instances replaced a few at a time                |
| How traffic moves               | All at once, when the switch flips                            | Gradually, as each new instance passes its check                 |
| Rolling back                    | Move the traffic back; the old copy is still running          | Run the swap again in reverse, one instance at a time            |
| Peak infrastructure             | Double, for the length of the release                         | A little above normal, set by how many spare instances you allow |
| Versions serving at once        | One, apart from the moment of the switch                      | Two, for the whole release                                       |
| What it needs from the platform | Control of the router, and room for a second copy             | A health check the scheduler reads, and more than one instance   |

Every row describes the code. None of them describes the data underneath it, which is the part that decides.

## "A rolling deployment is generally faster than a blue/green deployment"

That sentence comes from the AWS whitepaper on deployment options and it is the single most quoted line in this comparison, as of writing. It is also conditional in a way the quotation does not carry, which is why it has its own argument threads on Reddit from people who measured something else.

The condition is your startup cost. Blue-green pays the full boot of a second copy before any traffic moves, and pays it once, with every instance starting in parallel. Rolling pays a smaller boot repeatedly, in series, with a health check gate in the middle of every wave. Whichever total is smaller wins, and which one that is depends on a number about your application that the whitepaper does not know.

Take an application that needs 45 seconds to become ready: framework boot, a connection pool, a cache to populate. Across six instances rolled two at a time, that is three waves of 45 seconds plus the checks between them, so something over two minutes with the old version still serving throughout. Blue-green starts six instances at once, waits the same 45 seconds, and switches. The whitepaper's sentence holds cleanly for applications that are ready in a second or two. It stops holding for the one with a warmup, and the applications with warmups are the ones where release speed was a concern in the first place.

### The case of a rolling update with 1 replica

A rolling release needs somewhere to roll. With a single instance there are only two moves available and neither of them is rolling. Start the replacement first and you are briefly running two copies, which is blue-green with n=1\. Stop the old one first and there is a gap, which is downtime. Kubernetes names the second option honestly and calls it Recreate, as of writing.

This matters more than it sounds, because plenty of services never run more than one instance. A free tier, an internal tool, a staging environment, an API nobody has ever had to scale. On all of them the zero-downtime promise is kept by a mechanism that quietly needs one extra container for a few seconds, and whether the platform will run that container is the platform's decision. Find out which answer you have before a release finds out for you. The adjacent question of what a platform does with your containers when nobody is using the app is covered in [what cold starts are and why free-tier apps fall asleep](https://runsite.app/blog/cold-starts).

## The axis nobody compares on: how many versions your database sees

Everything above is a statement about stateless code. The comparison tables are honest within that boundary and they all stop at it. The moment your application has a database, which is to say almost always, a second constraint arrives that neither strategy removes and only one of them is usually credited with.

Rolling puts two versions of your code in production simultaneously by design, and both of them talk to the same database. Blue-green is usually no different, because the second environment is a copy of the application and not a copy of the data. Most descriptions of the pattern list the shared database among its drawbacks and leave it there, as though the difficulty were the sharing. The difficulty is that a schema change now has to be workable for the code on its way out as well as the code arriving.

Which means the migration pattern is the same under both strategies. Add the new column before anything reads it, deploy code that writes to both, remove the old column in a later release. Expanding and then contracting, rather than renaming in place. That mechanic and the reasons for it are set out in [git push to deploy](https://runsite.app/blog/git-push-to-deploy); what the two strategies change is only how long the overlap lasts, not whether you need to plan for one.

The switch is instant. The schema is not.

Blue-green's selling point is that a rollback is one movement of traffic. That is true of your code and false of your data. A dropped column stays dropped, and the previous release, pointed at a schema it was never written for, can fail in ways that are harder to diagnose than whatever you were escaping. Decide the recovery path for a migration before you need it, not while the traffic is moving.

None of this argues against either strategy. It argues that the choice between them is a smaller decision than the one about your schema, and that a comparison which never mentions the database is comparing the easy part. Where the database itself lives and what the host takes off your hands is a separate question, covered in [the jobs a managed Postgres solution absorbs for developers](https://runsite.app/blog/managed-postgresql-for-developers), and the database runs on [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql).

## What the health check decides, and what it only observes

A health check in a release is a gate, not a dashboard: the new version gets no traffic until it answers, and if it never answers, the traffic never moves. Both strategies share that principle. On a managed platform the contents of the check are frequently the only part of the release you control, so it deserves the attention the strategy debate usually absorbs.

### Liveness and readiness answer different questions

Readiness asks whether this instance should receive traffic right now. Liveness asks whether this process is broken badly enough to be killed and started again. The names are Kubernetes vocabulary, as of writing, but every scheduler that gates a release has both concepts under some spelling.

Wiring the same URL to both is common and produces a specific bad afternoon. A dependency has a bad thirty seconds. Readiness fails, which is correct, and the instance stops taking traffic. Liveness fails too, because it is the same endpoint, so the container is killed and restarted, and a restart takes longer than the blip did. What should have been a brief withdrawal from the load balancer becomes a cycle of containers dying and booting while the dependency recovers on its own.

### A check that only proves the port is open

The most common health endpoint in production is a route that returns 200 from a handler which touches nothing. It proves the HTTP server is listening. An application that booted with an unreachable database passes that gate, receives real traffic, and starts returning errors to real users while the deploy shows green in the dashboard. With no gate at all it is worse: traffic reaches an instance before it is even listening, and for those seconds visitors get a [502 Bad Gateway from the proxy](https://runsite.app/blog/502-bad-gateway-nginx).

A check that touches what the application needs to do its job fails instead, and the release stalls. That is the better outcome by a wide margin: a failed deploy with the old version still serving, rather than an outage with a successful deploy behind it. One cheap query and a ping to whatever cache you cannot serve without is usually enough.

There is a counterweight, and it bites in the opposite direction. A check that fans out to every downstream service turns any one of them having a bad minute into a failed deploy, and worse, into your instances being recycled during someone else's incident. The line that holds up is narrower than it first looks.

Check what this instance needs, not what the system needs

The endpoint should answer one question: can this container serve a request right now. Its own database connection and its own cache qualify. A third-party payment API, an email provider or a downstream team's service do not, however much you depend on them, because a check that includes them hands the decision to kill your containers to somebody else's uptime.

### Interval, timeout, and the deploy that fails for the right reason

Three numbers decide how patient the gate is: how often the check runs, how long a single attempt is allowed to take, and how many consecutive failures count as a verdict. Set them too tight and an application that needs 40 seconds to warm never passes, every release fails, and the reasonable conclusion is that the platform is broken. Set them too loose and a genuinely broken release takes minutes to be caught, which on a rolling deployment means it has been quietly replacing healthy instances the whole time.

The starting number is your own measured cold start with headroom on top, not the platform default, which was chosen for an application nobody had seen. Measure it once on a cold container rather than a warm one, and set the timeout above the slowest of several runs rather than the average.

## The blue-green deployment problem: cost, state, and the minutes in between

The cost objection is the one that gets raised first and it is real but bounded. Running two full production environments doubles the infrastructure you are paying for, and it does so for the length of the release rather than permanently. Where that bites depends on how you are billed. Metered by the second, doubling for four minutes is a rounding error. On fixed instances, the second environment may have to be owned all month for the few minutes a week it is used, which is a different conversation entirely.

The subtler cost is what the old copy is doing while it drains. Long-lived connections do not move when a router flips: websockets, server-sent events, a streaming response halfway through. You have two options and both are choices rather than defaults. Cut them, and users see a reconnect at a moment you chose. Wait for them, and the old environment stays alive and billable until the last one closes, which for a websocket application can be a long time after the switch. Either way the old process has to stop when it is told to: one that ignores SIGTERM is killed once the grace period runs out, and the deploy log shows [exit code 137](https://runsite.app/blog/exit-code-137) instead of a clean exit.

Background work has the same shape and less visibility. If your workers ship in the same release, flipping traffic does not drain a queue. A job that the old version picked up one second before the switch finishes on the old version, against whatever schema the new release just applied. Scheduled work has the same exposure: a job that fires during the overlap can land on either side of it, or, [when the scheduler lives inside the application, on both at once](https://runsite.app/blog/cron-job-multiple-instances). Where that work runs and how it is triggered is its own decision, and [scheduled jobs and cron on EU infrastructure](https://runsite.app/services/cron-jobs) is where it lives on Runsite.

Then there is a question the comparisons never raise, because they are written from inside a single region. Blue-green needs room for a second copy, and something has to find that room. If the platform satisfies it by placing the idle environment wherever there is capacity, then for the length of every release a complete copy of your production application is processing your production data somewhere you did not choose. For a hobby project that is a shrug. Under GDPR it is a transfer, and a transfer that lasts four minutes is still a transfer with the same paperwork behind it as a permanent one. The rules and what they require of a stack are covered in [where to store EU user data](https://runsite.app/blog/gdpr-data-residency).

The practical version of that is a question to ask a platform before you need the answer: not where the live environment runs, which every provider will tell you, but where the second one is allowed to appear.

## Where canary fits, and why it answers a different question

A canary release sends a small share of traffic to the new version, watches what happens to it, and then either widens the share or withdraws it. It reads as a third option on the same list as blue-green and rolling, and it is not quite the same kind of thing.

What canary needs is not a router feature. Splitting traffic by percentage is the easy half and most routers do it. What it needs is the ability to look at error rate, latency and whatever your application counts as a bad outcome for that 5% specifically, held against the other 95%, and to decide from the difference. Without that comparison you have a rolling release with a longer pause in the middle and someone watching a graph that averages both versions together.

So the honest ordering is that blue-green and rolling are release mechanics, while canary is a release policy that runs on top of one of them and leans on observability you may not have. The difference between blue-green and canary, put plainly: blue-green moves everyone at once and bets on the checks that ran before the switch, canary moves a few people first and bets on being able to measure what happened to them. If you cannot answer whether the 5% is doing worse than the 95%, the option is not available to you yet, whatever the router supports.

## Deployment strategies you can choose, and the ones you inherit

Nearly everything written about deployment strategies assumes you own the router. The Kubernetes documentation, the ArgoCD guides, the AWS whitepaper: all of it is written from the position of someone who configures a load balancer. On Kubernetes the entire strategy comes down to [two numbers on the Deployment object](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-update-deployment), as of writing.

yaml

```
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1        # extra instances allowed above the desired count
    maxUnavailable: 0  # instances allowed to be missing during the release
```

With maxUnavailable at 0 and maxSurge at 1 you get a rolling release that never dips below full capacity, one instance at a time. Push maxSurge up to the size of the whole deployment and you have approximately blue-green, since a full second set starts before any of the first set retires. The alternative type is Recreate, which stops everything and then starts everything, and is the only one of the three that is honest about producing downtime.

On a managed platform you usually see none of that, and losing sleep over it is the wrong reaction. Those knobs come attached to a cluster that somebody has to maintain. What matters is which parts of the decision the platform kept and which it handed to you.

| Layer                                            | Usually yours                                           | Usually the platform's                                 |
| ------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------ |
| Release strategy (rolling, blue-green, recreate) | Rarely                                                  | Chosen for you, and often not named anywhere in the UI |
| Health check path                                | Yes, and it is the highest-leverage thing you configure | —                                                      |
| Check interval and timeout                       | Often                                                   | Sometimes fixed, sometimes only on paid plans          |
| How many instances run during a release          | Rarely                                                  | Yes                                                    |
| Rollback to a previous release                   | Yes, when previous artifacts are kept                   | Whether they are kept at all, and for how long         |
| Where the second copy runs                       | Rarely visible                                          | Yes, and worth asking about explicitly                 |

The question on a managed platform is not which strategy to pick. It is which one you already have, and which of its settings were left to you.

That reframing is most of the practical value here. Read your platform's release documentation and find out what happens to traffic during a deploy, whether a failed check stops the release or merely reports it, and what the platform does with the previous version once the new one is serving. Those three answers tell you which strategy you are running, whatever it is called. On [EU-hosted web services with automatic rollback on a failed health check](https://runsite.app/services/web-services) the answers are rolling, stops it, and keeps it, which is the shape most applications want without having to ask for it.

## How Runsite handles it

Releases on [Runsite's web services, with rolling deploys behind a configurable health check](https://runsite.app/services/web-services) are rolling. New containers start alongside the ones already serving, traffic shifts once the health check passes, and a failed check returns to the previous healthy version instead of putting a broken build in front of users. The check path and its interval are yours to set, and that is the setting this article argues matters most.

The built image lives in Runsite's internal registry, while environment variables, secrets and health check configuration are attached to the deployment manifest separately from it, so configuration and artifact are not welded together at build time. Containers that fail are restarted automatically and rescheduled onto healthy nodes. Every pull request gets its own environment on its own URL without extra setup, and that is where a release you are nervous about should be seen first. Rollbacks and deploy history are on the Pro plan (€25/mo as of writing). A deploy takes around 30 seconds, with the full clone, build and release cycle usually under a minute as of writing. Servers are in Germany as of writing, so the question of where an extra copy of your application appears during a release has one answer instead of a shrug.

Setup details, including how to point the health check at something more useful than a bare 200, are in the [Runsite docs](https://docs.runsite.app).

## The short version

* Blue-green runs two complete copies and moves all traffic at once. Rolling runs one copy and replaces its instances a few at a time. Both promise zero downtime and both usually deliver it.
* Rolling is faster when your application starts quickly, which is the condition the AWS whitepaper's much-quoted line leaves out. With a 45-second warmup, blue-green's parallel boot wins.
* A rolling update with one replica is not a rolling update. It is either a brief second container or a gap, and which one you get is the platform's decision.
* Both strategies put two versions of your code in front of one database. The schema has to work for the outgoing version as well as the incoming one, and rolling code back does not roll a schema back.
* The health check is the part you usually control. Make it touch what this instance needs to serve a request, and nothing that belongs to somebody else's uptime.
* Blue-green's real costs are the doubled infrastructure for the length of the release, the connections and jobs that do not move when traffic does, and the second copy appearing in a region you did not pick.
* Canary is a policy rather than a mechanic. Without the ability to compare the small share against the large one, it is rolling with a longer pause.
* On a managed platform the strategy is usually chosen for you. Find out which one you have, then spend your attention on the health check.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is rolling update better than blue/green deployments?

Neither is better as a property of the strategy, and the question usually resolves on two things the comparison rarely names. The first is your startup time: rolling replaces instances in series with a health check between each wave, so an application that takes 45 seconds to warm spends several minutes mid-release, while blue-green boots a full second copy in parallel and pays that cost once. The second is capacity: rolling needs one spare instance, blue-green needs a complete duplicate for the length of the release, and on fixed-price plans that duplicate may have to be owned permanently. Underneath both sits the same constraint, which is that two versions of your code end up talking to one database, so the schema has to be workable for the outgoing version as well as the incoming one. On most managed platforms the strategy is chosen for you anyway, which makes the health check the decision that is genuinely yours.

### What's the difference between blue green and canary deployments?

Blue-green moves all traffic at once and stakes the release on the checks that ran before the switch. Canary moves a small share first, watches what happens to that share specifically, and then either widens it or withdraws it. The practical difference is what each one needs from you. Blue-green needs a router that can switch and room for a second environment. Canary needs the ability to compare error rate and latency for the 5% against the 95% and to decide from the difference, and that is an observability capability, not a routing one. Without it, splitting traffic by percentage gives you a rolling release with a longer pause in the middle. That is also why canary is better described as a policy running on top of a release mechanic than as a third mechanic beside the other two.

### Is blue green deployment risky?

The switch itself is the safest part, since the previous environment is still running and moving traffic back is one action. The risk sits in everything that is not a stateless HTTP request. A schema change applied for the new version is not undone by moving traffic back, so an instant rollback of the code can leave the old version pointed at a database it was never written for. Long-lived connections such as websockets or streaming responses do not follow the router, so they are either cut at a moment you chose or the old environment stays alive until they close. Background jobs picked up just before the switch finish on the old version against the new schema. And if the platform places the second environment wherever it has capacity, a complete copy of your production processing runs in an unspecified region for the length of the release, which under GDPR is a transfer regardless of how briefly it lasts.

### What are the disadvantages of blue green deployment?

Three of them, in descending order of how often they are mentioned. The infrastructure cost is the obvious one: two full production environments run at the same time, so resource use doubles for the length of the release, and on fixed-price instances rather than per-second metering that second environment may need to be paid for all month. The shared database is the one that causes actual incidents, because the second environment is a copy of the application and not of the data, so both versions read and write the same schema and the fast rollback covers only the code. The third is drainage: connections, streams and in-flight background jobs do not move when a router flips, so the old environment either has to be cut off or kept alive until it empties. None of these is a reason to avoid the strategy, but a comparison that lists only the doubled cost has covered the cheapest of the three.

Keep reading

---

# VPS vs PaaS Cost: The €5 Server Is Not €5

> A European VPS against a PaaS bill, priced from the invoice rather than the pricing page: the missing line items, where break-even sits, and who the processor is.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. VPS vs PaaS Cost: What a €5 Server Actually Costs You

DeploymentAugust 19, 202614 min read

# VPS vs PaaS Cost: What a €5 Server Actually Costs You

The price gap between a raw VPS and a managed platform is smaller than the pricing pages suggest. This itemises both sides: the hidden line items, the jobs, and who the processor is.

## The short version

The gap is smaller than the pricing pages suggest. A €5.49 Hetzner server is not the invoice: an IPv4 address, backups and a second machine land on top, and the price stability argument broke on 15 June 2026\. What the gap buys is a list of recurring jobs, not features — TLS renewals, patching, an untested restore.

[Teo Marquardt](https://runsite.app/about#author)

The advertised price of a European server is about the same as a sandwich. As of writing, Hetzner's CX23 in Falkenstein or Nuremberg gives you 2 vCPU, 4 GB of RAM, 40 GB of NVMe and 20 TB of traffic for €5.49 a month. That number is on the pricing page, it is real, and it is not what the server costs.

Add the IPv4 address at €0.50, because the advertised price does not include one and your application needs to be reachable. Turn on automatic backups, which are billed at 20% of the instance price, and add €1.10\. You are at €7.09 net. If you are a private individual or a business without a VAT ID, add 19% German VAT and the invoice reads about €8.44\. The sandwich was €5.49; the thing running your application is half as much again.

This matters because the entire VPS versus platform debate is conducted in advertised prices. One of the threads still ranking for this question is titled "Confusion choosing a hosting service... VPS or PAAS". Another, from r/SaaS, names the part people actually argue about: "VPS vs PaaS battle (especially time & cost)". The threads are long and the answers are confident in both directions, and almost none of them get past the first line of the price list.

This article gets past it. The arithmetic is written out, the jobs inside the price gap are listed one by one, and there is a section on who is legally responsible for the data, which the usual debate never raises. There is no verdict at the end. The honest answer depends on numbers only you have.

## "A VPS at $20/month that takes 4 hours/month to maintain costs more than a PaaS at $80/month"

That sentence is from a comment in r/devops, and Google quotes it word for word on both the "vps vs paas" and "vps vs paas cost" results pages. It is currently the most-read summary of this decision on the internet. The full version ends "...if your time is worth anything."

It is directionally right and worth finishing, because the number hiding inside it is much smaller than the rhetoric suggests. The gap between $20 and $80 is $60\. Divide by four hours and the break-even rate is $15 an hour. That is what "if your time is worth anything" resolves to: fifteen dollars an hour. Above that the platform is cheaper, below it the server is.

Which is a low bar, and a suspiciously convenient one for whichever side is quoting it. The two inputs are where the argument actually lives, and both of them are softer than they look.

### Why your hourly rate is the wrong variable to start with

The heuristic asks what your time is worth and treats the four hours as given. In practice your hourly rate is the number you know and the four hours is the number you are guessing at, so the guess carries the whole calculation.

Ask ten people how long a VPS takes to run and the answers range from zero to a day a month, and most of them are telling the truth about their own situation. A single Django app on a box that was set up carefully, behind a reverse proxy nobody has touched in a year, serving traffic that never spikes, genuinely takes close to no time. Months go by. The person who says they have run theirs for three years and spent maybe two hours total is not exaggerating.

Then a certificate does not renew, or a kernel advisory lands on a Friday, or the disk fills with logs nobody rotated, and that month costs six hours. The annual average might still be under an hour a month. The average is not the thing that hurt.

So the decision has a different shape than the monthly comparison suggests. You are choosing between two distributions: a predictable bill in euros, or a mostly predictable bill in your own attention with an occasional spike attached. Most of this article is about pricing that spike.

## What a €5.49 server costs once it is running production

Before any of that, the advertised price needs correcting, because every comparison you will read starts from it and none of them itemise it. These are Hetzner's own line items as of writing, and the same pattern holds at Scaleway, OVHcloud, UpCloud and Exoscale with different numbers.

| Line item           | Cost as of writing                  | Why it appears                                              |
| ------------------- | ----------------------------------- | ----------------------------------------------------------- |
| CX23 instance       | €5.49/mo                            | 2 vCPU, 4 GB RAM, 40 GB NVMe, 20 TB traffic                 |
| IPv4 address        | €0.50/mo                            | Not included; IPv6 alone is not enough for most public apps |
| Automatic backups   | 20% of the instance price, so €1.10 | Off by default, and the alternative is writing your own     |
| Snapshots           | €0.0143/GB/mo                       | Only if you keep images for rollback or cloning             |
| Traffic above 20 TB | €1/TB in the EU                     | Generous quota, but metered above it                        |
| VAT                 | 19% in Germany without a VAT ID     | Applies to consumers and non-registered businesses          |
| Running total       | €7.09 net, about €8.44 with VAT     | One machine, before any of your time                        |

The advertised price and the invoice are different numbers, and every comparison that quotes the first one is comparing the wrong thing. Figures from Hetzner's price list as of writing; check the current one before relying on them.

None of this makes the server expensive. €8.44 for a production-capable machine in Germany is an excellent price and Hetzner deserves the reputation it has. The point is narrower: the number every comparison starts from understates what one machine costs to run by about 29% for a VAT-registered business, and by about 54% for anyone who owes the VAT. Either way the shortfall lands before the argument reaches the part that is actually contested.

## The VPS vs PaaS cost calculator, written out

"Vps vs paas cost calculator" is one of the two searches Google suggests underneath these results, and there are tools that will run it for you. The calculation is small enough to write down. Take the monthly gap between the two options, divide by your hourly rate, and you get the hours the gap buys. If the server takes less time than that, the server is cheaper.

The gap depends entirely on what you are comparing, which is why the single-server version is misleading. One machine at €7.09 against a €50 managed plan with matching 2 vCPU and 4 GB leaves €42.91, and that is a genuinely large gap that buys real hours. A working production setup is rarely one machine. Add a staging environment and give the database its own host, which is the correct call the moment the data matters, and it is three machines at €21.27 net. Against a managed platform running the application at €50 and a managed Postgres at €5, the gap is €33.73.

| Your rate | Hours the gap buys: one server (€42.91) | Hours the gap buys: realistic setup (€33.73) |
| --------- | --------------------------------------- | -------------------------------------------- |
| €15/hour  | 2.9                                     | 2.2                                          |
| €30/hour  | 1.4                                     | 1.1                                          |
| €50/hour  | 0.9                                     | 0.7                                          |
| €75/hour  | 0.6                                     | 0.4                                          |
| €100/hour | 0.4                                     | 0.3                                          |

Break-even in hours per month. At any professional rate the budget is well under an hour, and the realistic setup tightens it further while tripling the number of machines you maintain.

The table is still not a decision, because maintenance is a list of jobs rather than a number you can look up. The honest version of this calculation is to go through the list and mark the ones you will actually do.

## What the price gap buys: a list of jobs, not features

Comparison pages tend to list what a platform has. Build pipelines, preview environments, one-click databases. That framing makes the platform sound like a shop, when the useful framing is a rota. Every job below exists in both scenarios. Nothing on the list disappears because you chose a server. The only thing that changes is whose calendar it lands in.

| The job                                      | On a raw VPS                                                                       | On a managed platform                                   |
| -------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------- |
| Issuing and renewing TLS certificates        | Yours: install an ACME client, wire the renewal hook, notice when it stops working | Included, and the failure mode is somebody else's pager |
| OS security updates and kernel reboots       | Yours, including deciding when a reboot is acceptable                              | Included below the container line                       |
| Language and runtime version upgrades        | Yours, on a machine that is also serving traffic                                   | Rebuild and redeploy, usually a one-line change         |
| Health-gated release and rollback            | Yours to build: two containers, a proxy switch, a check, a way back                | Standard, though which strategy you get varies          |
| Log rotation and disk headroom               | Yours, and the failure is a full disk at an inconvenient hour                      | Included, with retention as a setting                   |
| Secrets storage and rotation                 | Yours: a file on the box, or a real secret store you now run                       | Attached to the service, injected at runtime            |
| A second environment for staging or previews | A second server, and another IPv4, and another backup line                         | Usually a per-branch environment, sometimes metered     |
| Backups, and a restore you have tested       | Yours end to end, including the off-box copy                                       | Backups included; the restore test is still yours       |

Eight jobs that exist either way. The price gap is what it costs to move them off your list, not what it costs to make them exist.

Another vendor blog in this same set of results puts it in one line: "PaaS costs more per month but saves you hours. A VPS costs less but hands you the whole job." That is accurate, and the table above is what "the whole job" contains once you write it down.

### TLS, and why 90 days is a schedule rather than a setting

Let's Encrypt certificates are valid for 90 days, a short lifetime chosen deliberately: a short lifetime forces automation. The automation is good, and on a well-configured box the renewal runs on a timer and you never think about it.

bash

```
# The renewal itself is one line and it works.
sudo certbot renew --quiet

# So does this, until a kernel update needs a reboot you have to schedule.
sudo unattended-upgrades --dry-run --debug
```

What the automation does not cover is the day the renewal hook fails quietly because the web root moved, or the reload command changed name after a distribution upgrade, or port 80 got firewalled during unrelated hardening. The certificate does not fail on renewal day. It fails 90 days later, on the expiry date, usually to a user rather than to you. The whole list follows this pattern: the routine case is automated and cheap, and the cost concentrates in the moment the automation stops being true without telling anyone.

### The second machine you did not budget for

Three CX23s with IPv4 and backups come to €21.27 net a month, which is still cheap in absolute terms. What changes is the ratio the argument rests on. A comparison that starts at seven-to-one and quietly becomes two-and-a-half-to-one has lost most of its force, and the maintenance side has tripled while the bill stayed small.

## "Cost predictability is a strong point for VPS" — until 15 June 2026

That first phrase is lifted from another page in these results, and it is the strongest argument the server side has. A fixed monthly price does not spike when your traffic does, and against usage-based platform billing that is a real advantage. Anyone who has watched a metered invoice arrive after a launch knows why people care about it.

It is also the claim that took the most direct hit this year. Hetzner's own price-adjustment notice sets the change at 15 June 2026, 8 AM CEST, and the increases were nowhere near uniform. Cost-optimised instances moved least: the Arm CAX11 went from €4.49 to €5.99, about a third. The dedicated-vCPU CCX line moved most, with CCX13 going from €15.99 to €42.99, an increase of 169%. The shared-AMD CPX line sits between the two, with CPX62 going from €50.49 to €129.99\. Those are Hetzner's own published figures rather than an estimate, and all of them are net of VAT.

Existing instances kept their old pricing, so nobody's invoice changed overnight. The catch is in the word "rescales": the moment you resize an instance, you are buying at the new price. Size a CCX13 in 2025, find you need more memory in 2026, and the resize hands you the current price on a plan that went up 169% in the meantime, on the same provider, in the same rack.

This is not an argument against Hetzner, who remain among the best value in Europe even after the change. It is an argument against treating a fixed monthly price as a fixed long-term cost. Predictability on a VPS means the invoice is stable between price changes, which is the same guarantee a managed platform on fixed plans offers, and neither of them is a promise about next year. The [difference between fixed and usage-based platform billing](https://runsite.app/blog/render-vs-railway) is a separate axis from this one and worth understanding before comparing any two options.

## The Hidden Cost Is Cognitive Load, Not Servers

That heading is lifted word for word from another article in these results, and it names what the hourly-rate model misses. The cost of running your own server does not distribute evenly across the year in tidy fifteen-minute increments. It arrives in lumps, at times you did not pick, while you were doing something else.

The shapes it arrives in are familiar enough. A CVE in a library your base image depends on, published on the day you planned to ship a feature. A disk at 98% because a debug log level got left on three weeks ago. A process killed by the OOM killer overnight with no supervisor configured to bring it back. An IP that ends up on a blocklist because of what a previous tenant of that address range did.

Each of these is solvable and none is hard. The cost is that they interrupt, arrive without notice, and compete with the work you are paid for. If you are one person running a product, price the variance rather than the median month. The variance is what decides whether a bad week becomes a bad launch.

### The restore you have never tested

One line in the jobs table deserves singling out, because people believe they have handled it when they have not. Taking a backup is easy, and at 20% of the instance price most people running a VPS do turn it on. Restoring is a different exercise, and an untested restore is a plan rather than a backup.

The failures are dull and common. The dump is of the wrong database. The job stopped six weeks ago and the alert went to an address nobody reads. The backup sits on the same disk as the thing it protects. The encryption key was in an environment file on the machine that died. A managed platform moves some of this off your list, though not the part where you confirm that a restore produces a working application. If you want this argument on the database side specifically, [the self-hosted versus managed trade-off for PostgreSQL](https://runsite.app/blog/self-hosted-vs-managed-postgresql) works through it in detail.

## Is a VPS just a VM?

This one comes up in Google's own People Also Ask box, and the confusion underneath it is why the cost debate goes in circles.

Technically, yes. A VPS is a virtual machine on shared physical hardware with dedicated slices of CPU, memory and disk, and what you receive is a kernel, a network interface and root access. That is the whole of it.

What you are not buying is anything above that line, and this is where the two sides of the comparison stop being comparable. A managed platform is not a faster way to get a virtual machine. It is a different product that happens to run on one. Deploying to a platform buys you a build system, a container registry, a router with certificates attached, a release mechanism with a health check in front of it, a supervisor that restarts what crashes, log aggregation and somewhere for secrets to live. Pricing those against a VPS is pricing a finished kitchen against a plot of land, and both numbers are correct for what they describe.

So the question that decides this is which of those layers you want to own, given that owning them is a legitimate choice with real upsides. [What a deploy pipeline is actually made of, layer by layer](https://runsite.app/blog/git-push-to-deploy), is worth reading if you plan to build one.

## Falkenstein is not a jurisdiction argument on its own

Nothing in the top results for these searches raises this, and it is the part with legal consequences rather than budgetary ones.

Putting your application on a server in Falkenstein or Nuremberg or Gravelines does mean the data sits on European soil, and that is worth having. It does not settle the compliance question, because data protection law does not primarily ask where the disk is. It asks who is processing personal data, on whose instructions, under what written agreement.

Who is the processor in each scenario

On a raw VPS, you are. The hosting company is your processor for the infrastructure, and everything above the hypervisor is yours as controller: the application, the database, the logs, the backups, the access controls and the records that show what you did. On a managed platform, the platform is a processor for the layer it operates, which puts a signed data processing agreement, a documented sub-processor list and defined security measures between you and a regulator's question. Location has nothing to do with that difference. What matters is who carries an Article 28 obligation for the platform layer, and on a VPS the answer is nobody but you.

Self-hosting is not non-compliant, and plenty of teams run their own infrastructure and meet their obligations properly. The argument is that the compliance work is a line item on the same list as certificate renewal and log rotation, and it belongs in the comparison rather than in a footnote. [What a data processing agreement commits each side to](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) is the detail behind that callout, and [the three separate responsibility lines between IaaS and PaaS](https://runsite.app/blog/paas-vs-iaas-responsibility) covers why the legal one does not run where the operational one does.

A second-order point catches people out on the platform side too. A European region is not the same thing as a European operator, and a US-registered company offering an eu-central region is still a US-registered company whose corporate entity answers to a different legal system. [The distinction between an EU region and an EU provider](https://runsite.app/blog/eu-vs-us-hosting) is worth checking before assuming the residency question is closed.

If the comparison you actually need is one managed platform against another rather than a platform against a server, the same small app is priced on seven of them in [PaaS pricing compared](https://runsite.app/blog/paas-pricing-compared).

## When the raw server is the right answer

Often it is, and a comparison that never reaches this section is an advertisement. The server wins clearly in these situations:

* Someone on the team enjoys this work. Not tolerates it, enjoys it. Systems administration done by a person who likes doing it is fast, reliable and cheap, and no cost model captures that.
* The workload is stable and boring. One application, predictable traffic, infrequent deploys, no ambition to add three more services this quarter. This is the case where months genuinely pass without anyone touching the box, and where unattended-upgrades plus a certbot timer really do cover the whole job for a year at a time.
* You need something a platform will not run. A persistent WebSocket or game server that holds connections for hours, a GPU, licensed software that checks a MAC address at startup, a kernel module for a VPN, or anything that needs to bind a raw port rather than sit behind an HTTP router.
* The bill scales badly on the platform side. A media or download-heavy app that pushes several terabytes a month is the clearest case: 20 TB is inside a €5.49 server's included traffic and is an egress line item almost everywhere else. Per-seat pricing on a team that grew does the same thing more quietly.
* You are learning. Running your own server teaches you what platforms abstract away, and that knowledge makes you better at using them afterwards. Charge it to the education budget rather than the infrastructure budget.
* Money is a binding constraint rather than a preference. A side project with no revenue and a founder with more time than money is exactly what the €5.49 server was built for, and at that point the six hours in a bad month cost nothing you were charging for anyway.

Where it goes wrong is the case in the middle: a small team, nobody dedicated to operations, a product with paying customers, and a server that was set up during a week when someone had time and has been drifting since. That is where the four-hour estimate stops being an estimate and the variance shows up. If you recognise your setup in that sentence, [the European platform options and what each shape of them costs](https://runsite.app/blog/european-heroku-alternative) are worth a look before the next incident makes the decision for you.

There is a third option that is a real choice rather than a compromise. Installing a self-hosted control plane such as Coolify on your own server buys push-to-deploy and certificates without a platform bill, at the cost of a new layer you now maintain. [That trade-off has its own ledger](https://runsite.app/blog/coolify-vs-managed-paas), and it is a different calculation from this one, which assumes there is no control plane at all.

## How Runsite handles it

For the managed side of this comparison, concretely, and with the same itemising applied. [Runsite runs managed web app hosting in the EU](https://runsite.app/services/web-services) at €3 for the entry Nano plan with 0.1 vCPU and 256 MB, €5 for 0.5 vCPU and 512 MB, €12 for 1 vCPU and 1 GB, €25 for 1 vCPU and 2 GB, and €50 for the 2 vCPU and 4 GB that matches a CX23, all as of writing. There is no separate charge for an IP address, TLS, or the build system, and no backup surcharge on the application tier. Nano keeps active applications warm rather than sleeping them after fifteen minutes, and a container on it only sleeps after 14 days without a single request; Starter and above never sleep.

Against the jobs table: connect a repository on GitHub, GitLab or Bitbucket and a webhook builds on every push to your branch, with the framework detected automatically for Next.js, Django, Rails, FastAPI and others, or your own Dockerfile used when you supply one. Releases are rolling behind a configurable health check, and a failed check returns to the previous healthy version instead of putting a broken build in front of users. HTTPS is automatic on a runsite.app subdomain or your own domain. Every pull request gets its own environment on its own URL, which answers the second-machine problem without a second machine, a second IPv4 or a second backup line. A deploy takes around 30 seconds, with the full clone, build and release cycle usually under a minute. Rollbacks and deploy history are on the Pro plan.

On the jurisdiction question: servers are in Germany as of writing, and containers, environment variables, build artifacts and logs stay inside the EU. A signed GDPR data processing agreement comes with every account, which is the specific thing a VPS cannot give you, because on a VPS that obligation has nowhere to sit except with you. Neighbouring pieces are on the same invoice rather than on the same disk: [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql) from €3 a month with backups you do not operate, [Redis for caches and job queues](https://runsite.app/services/redis) from €5, scheduled jobs, and [object storage with no egress charges](https://runsite.app/services/s3-storage).

Being fair about the comparison: at the top of the table Hetzner gives you more disk and more traffic for the money, and if the jobs list is work you want to do, that is the better deal. If none of it should be landing in your calendar, the €5 starting credit covers a Nano plan without a card. Setup details are in the [Runsite docs](https://docs.runsite.app).

## The short version

* The advertised price is not the invoice. A €5.49 Hetzner CX23 becomes €7.09 net once you add the IPv4 address at €0.50 and backups at 20% of the instance price, and about €8.44 with German VAT, as of writing.
* Every comparison you will read starts from the advertised number, which understates the cost of one running machine by about 29% net of VAT, and by about 54% for anyone who owes it.
* The much-quoted Reddit rule of thumb resolves to $15 an hour. That is what "if your time is worth anything" means once you divide the $60 gap by four hours.
* At a professional rate the gap buys under an hour of maintenance a month, and less than that on a realistic multi-machine setup.
* The one-server comparison is the wrong one. Add staging and a separate database host and you are at €21.27 net across three machines, three times the maintenance surface, and a gap of about €33.73 rather than €42.91.
* Price the variance, not the median. A stable box takes almost no time for months and then costs six hours on a Friday. The bad week is what the platform premium insures against.
* Fixed monthly pricing is not a long-term guarantee. Hetzner's price-adjustment notice raised prices for new orders and rescales from 15 June 2026: CCX13 went from €15.99 to €42.99, an increase of 169%, while CAX11 moved only from €4.49 to €5.99\. Existing instances kept their price, but rescaling buys at the new one.
* Nothing on the jobs list disappears when you choose a server. Certificates, kernel updates, log rotation, secrets, a staging environment, health-gated releases and a tested restore exist either way. Only the calendar changes.
* A VPS is a virtual machine and a platform is not a faster virtual machine. You are pricing a plot of land against a finished kitchen, and both numbers are right for what they describe.
* The server's location is not the compliance answer. On a VPS you carry the Article 28 obligations for everything above the hypervisor; on a managed platform that obligation is attached to the platform layer, in writing.
* The raw server wins outright when someone enjoys the work, the workload is stable, the platform will not run what you need, or money is the binding constraint. It goes wrong for small teams with paying customers and nobody on operations.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### How much does a VPS cost?

Less than most people expect, and more than the advertised price. As of writing, Hetzner's CX23 with 2 vCPU, 4 GB of RAM, 40 GB of NVMe and 20 TB of traffic lists at €5.49 a month net in Nuremberg or Falkenstein, and the entry tiers at Scaleway, OVHcloud, UpCloud and Exoscale sit in a comparable range. The listed figure is not the invoice: an IPv4 address is €0.50 a month on top, automatic backups are billed at 20% of the instance price, snapshots are €0.0143 per GB, and VAT applies at 19% in Germany for anyone without a VAT ID. One machine with an IP and backups comes to €7.09 net, or about €8.44 with VAT. A realistic production setup is usually two or three machines once you add a staging environment and give the database its own host, which puts the running cost near €21 net and triples the operational surface.

### Is a VPS just a VM?

Technically yes. A VPS is a virtual machine on shared physical hardware with dedicated slices of CPU, memory and disk, and what you receive is a kernel, a network interface and root access. The reason the question keeps getting asked is that people are usually comparing a VPS with a managed platform, and a platform is not a virtual machine with a nicer control panel. It is a build system, a container registry, a router with certificates attached, a health-gated release mechanism, a process supervisor, log aggregation and a secret store, all of which happen to run on virtual machines somewhere. When the two get priced against each other, the gap is mostly the cost of those layers rather than a markup on the same product.

### Is VPS better than cloud hosting?

The comparison works better rephrased as what you are willing to operate, since better depends entirely on which jobs you want on your own list. A VPS gives you a fixed monthly price and full control, and hands you certificate renewal, kernel updates, log rotation, secrets, release orchestration, monitoring and backup restores. A managed platform takes those off your list and charges a premium that, at professional hourly rates, works out to under an hour of your time a month. The strongest argument for the server is cost predictability, though that is a guarantee between price changes rather than a long-term one: Hetzner's price-adjustment notice raised prices for new orders and rescales from 15 June 2026, with CCX13 going from €15.99 to €42.99\. The strongest argument against the server is variance, because the routine month is cheap and the bad week is not, and the bad week arrives without notice.

### How much does VPS hosting usually cost?

The list price and the real cost are different numbers, and the distance between them is the subject of this comparison. The list price for a small European VPS is a few euros a month. The real cost adds the line items the list price leaves out, which are the IPv4 address, backups at a percentage of the instance price and VAT where it applies; then the additional machines a working setup needs; then the maintenance hours at whatever your time is worth; then the irregular incidents that never show up in a monthly average, such as a security advisory landing on a busy day, a disk filling at three in the morning, or a certificate renewal that failed silently 90 days earlier. A practical way to price it is to take the corrected monthly gap against the managed alternative, divide by your hourly rate, and ask whether the server stays under that many hours in a bad month rather than an average one.

Keep reading

---

# When to Use Redis as a Cache — and When Not To

> When a Redis cache pays for itself, when it makes an app slower and less correct, the four failures that only show up in production, and what it costs.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. When to Use Redis as a Cache (and When It Makes Things Worse)

CachingAugust 21, 202612 min read

# When to Use Redis as a Cache (and When It Makes Things Worse)

A cache is a second copy of your data that is allowed to be wrong. This is the decision frame: what to cache, what never to, the four failures that only appear in production, and why a cache full of sessions is a compliance object.

## The short version

A cache is a second copy of your data that is allowed to be wrong, so measure what is actually slow before adding one. Skip it when the data must be correct at the moment it is read, when writes outnumber reads, or when one instance already holds everything in memory. A cache holding sessions is a compliance object.

[Teo Marquardt](https://runsite.app/about#author)

A cache is a second copy of your data, kept somewhere faster, that is allowed to be wrong. The last clause is the whole subject. Everything that goes well with a cache and everything that goes badly comes from the same property: there are now two copies of the truth, and nothing in the system guarantees they agree.

The question of when to use Redis as a cache is old enough that the results page answering it is visibly aging. As of writing, the top organic result in Germany is a thread on r/webdev from six years ago with ten comments in it, and the fifth is a Stack Overflow question roughly twelve years old. Most of the pages between them belong to Redis itself. They are good pages. A vendor's documentation is not where you find out when its product is the wrong choice.

So this stays at the level of the decision rather than the implementation. What earns a cache, what should never go in one, the four failures that only introduce themselves once real traffic arrives, and the part almost nobody writes down: a cache holding session tokens is a store of personal data, with the residency and processor obligations that follow from that. Which engine you run it on is [a separate comparison, and one the licence changes of 2024 rewrote](https://runsite.app/blog/redis-vs-memcached).

## Is Redis a cache or a database?

Both, and that is the root of most of the trouble. Google carries this question in the related searches of both results pages for this topic, in that exact wording, which suggests it is not only beginners asking it.

Redis has no mode switch. The same program serves as a throwaway cache and as a system of record, and which one you have is decided by three configuration choices you may never have made deliberately: whether keys carry an expiry, what happens when memory fills up, and whether anything is written to disk. Get the combination wrong in one direction and you have a database that quietly discards rows. Get it wrong in the other and you have a cache that stops accepting writes at the worst possible moment, which is a scenario further down this page.

Think of it as a data structure server that happens to live on the network, with sub-millisecond access to strings, hashes, lists, sets, sorted sets and streams. Caching is simply the most common thing people do with one.

The rule underneath everything below

A cache is a second copy of your data. Every problem in this article is some version of the two copies disagreeing, or of the second copy running out of room. If you keep that sentence in mind, most of the failure modes stop being surprising.

## Before the cache: what is actually slow?

A cache is often the second-best answer to a problem that has a cheaper first answer. Adding one is easy enough that it gets reached for before anyone has looked at where the milliseconds go, and the result is an application with the same underlying problem plus an extra network hop, an extra dependency and an extra copy of the data to keep honest.

| What is actually slow                             | What a cache does about it                                       | The cheaper move to try first             |
| ------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------- |
| One query scanning a large table                  | Hides it behind a TTL until the next miss, when it is slow again | An index. The query plan names the column |
| Forty small queries inside one request            | Turns forty database round trips into forty cache round trips    | Fetch them in one query                   |
| Every request opening a fresh database connection | Nothing at all                                                   | Connection pooling                        |
| Connections exhausted under load                  | Nothing, and adds a second pool to size                          | Fix the pool, then look again             |
| The first request after an idle period            | Nothing. The cache is cold too                                   | Keep the container warm                   |
| A response computed from data that changes hourly | Removes the work entirely                                        | This one is the cache                     |

Four of these six have an answer that is cheaper, simpler and does not add a second copy of the data.

The two database rows get conflated constantly, and they are different problems. Running out of connections produces [the too many clients already error and its own diagnosis path](https://runsite.app/blog/postgres-too-many-clients-already), and a cache does not help with it: your application still opens a connection on every miss, and misses arrive in bursts. The fix is [connection pooling in front of PostgreSQL](https://runsite.app/blog/pgbouncer-connection-pooling), which is a different layer solving a different problem. Do that first, then measure again, because the cache you were about to add may turn out to be unnecessary.

Slow first requests deserve the same scepticism. If the complaint is that the application is fine once it is warm, the problem is [the container going to sleep and being rebuilt on demand](https://runsite.app/blog/cold-starts), and a cold cache makes that first request slower rather than faster.

## When to use Redis as a cache

Four shapes of workload reliably pay for the second copy. They share a property: the data is read far more often than it changes, or it is not really data at all but a counter or a token.

Repeated expensive reads is the classic case. A product catalogue, a pricing table, a rendered fragment of a page, an aggregate that takes two seconds to compute and is requested four hundred times an hour. What makes it worth doing is the ratio between reads and writes. The size of the object barely enters into it.

Sessions are the case that grows on you. As soon as an application runs on more than one instance, in-process session storage stops working, because the second request lands on a different container and the user appears logged out. Redis solves this by being outside all of them, and the expiry that makes people nervous elsewhere is exactly the behaviour a session wants. Whether sessions belong there or in the database you already run is decided less by speed than by [expiry, sign-out and what ends up in your backups](https://runsite.app/blog/redis-vs-database-for-sessions).

Counters and limits are a fit for a different reason. Rate limits, view counts, feature-flag rollouts and queue depths are all small numbers updated concurrently, and Redis gives you atomic increments without a transaction against your primary database. Nobody would call this caching, and it lives on the same instance.

Then there is anything derived. If a value can be recomputed from data you still hold, losing it costs CPU rather than correctness, and that is the property that makes a cache safe to lose. Apply the test literally: if the instance were wiped right now, what breaks?

| Data                                          | Cache it?                                   | Why                                                   |
| --------------------------------------------- | ------------------------------------------- | ----------------------------------------------------- |
| Catalogue read on every page, updated nightly | Yes                                         | Read-heavy, and an hour of staleness costs nothing    |
| An aggregate that takes seconds to compute    | Yes                                         | Losing it costs CPU, not correctness                  |
| User sessions across several instances        | Yes, with the TTL set to the session length | Shared by definition, and expiry is the feature       |
| Rate-limit and quota counters                 | Yes                                         | Ephemeral by nature, atomic increments                |
| Permission and role checks                    | Carefully, with a short TTL                 | A stale permission is a security bug on a timer       |
| Stock level or balance at checkout            | No                                          | Read it from the source that owns it                  |
| Anything you could not recompute              | No                                          | That is a database, and it needs to be treated as one |

## When not to use Redis cache?

Four cases, and the first two account for most of the regret.

### The data has to be right at the moment it is read

Balances, stock levels, permissions, anything a decision is made on. A cache is a window onto the past whose width you chose when you picked the TTL, and 300 seconds of stale permissions is a security bug with a five-minute fuse. If the answer to "what if this value is thirty seconds old" is anything other than "nothing much", the value does not belong behind a cache without an explicit invalidation path.

### Writes outnumber reads

A cache pays for itself across repeated reads of the same key. Data written frequently and read once or twice gives you the write cost, the invalidation cost and the memory cost, against almost no benefit. Changing the write pattern does not rescue this, and [the four patterns and what each one costs on the write path](https://runsite.app/blog/cache-aside-write-through-write-behind) are worth reading before deciding otherwise. The pattern to watch for is a key that is updated on every request and read on the same request.

### One instance, and the data fits in memory

This one is unpopular and keeps being true. If your application runs as a single process and the working set fits in its own memory, a plain in-process map is faster than Redis, because it does not cross a socket. The r/dotnet thread that Google cites in its AI Overview for this topic reaches the same conclusion from experience rather than theory. Redis starts winning the moment there is a second instance, or the moment the cache needs to survive a deploy.

### Nobody has measured yet

Adding a cache before you know which call is slow buys you a dependency, a second copy of the data and a new failure mode, in exchange for a guess. The table earlier in this article exists because four of its six rows have a cheaper answer than caching, and you cannot tell which row you are in without looking.

## Four ways a cache fails in production

None of these show up in development. All four show up on the day traffic arrives, and three of them announce themselves as a failure in the application rather than in the cache, which is what makes them slow to diagnose. The first one has a whole article behind it: [what the OOM error means and which eviction policy you actually want](https://runsite.app/blog/redis-oom-command-not-allowed) covers the eight policies and the cache-or-store question that decides between them. Watch for it especially if the same instance also holds [rate-limit counters](https://runsite.app/blog/rate-limiting-429-retry-after), which look like cache and are not.

### maxmemory-policy noeviction

The expectation almost everyone brings to a cache is that a full cache throws away old entries. Redis does that only if you tell it to. In [the upstream redis.conf](https://github.com/redis/redis/blob/unstable/redis.conf), as of writing, the default eviction policy is `noeviction`: once memory is exhausted the server stops accepting writes and returns an error to any command that would use more of it. Reads keep working, so the application looks half alive.

text

```
127.0.0.1:6379> SET session:9f21c4a7 "{...}"
(error) OOM command not allowed when used memory > 'maxmemory'.

127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"

127.0.0.1:6379> INFO memory
used_memory_human:256.00M
maxmemory_human:256.00M

127.0.0.1:6379> INFO stats
evicted_keys:0          # nothing was ever evicted, and that is the tell
```

The line to read is `evicted_keys:0`. A cache that has never evicted a key while sitting at its memory ceiling is not a cache, it is a full disk with a network interface. Managed providers and distribution packages frequently override the upstream default, so check the running value rather than assuming either way. Which policy to switch to, and why the choice between recency and frequency matters more than it looks, is its own subject.

### MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk.

The second failure is the mirror of the first: persistence nobody asked for. Redis ships with [RDB snapshotting](https://redis.io/docs/latest/operate/oss%5Fand%5Fstack/management/persistence/) enabled, and with `stop-writes-on-bgsave-error` set to `yes`, so a failed background save makes the server refuse writes until the problem is fixed. The usual cause is the disk filling, and the usual reaction is confusion, because the team believed they were running a cache with nothing on disk at all.

text

```
127.0.0.1:6379> SET user:1042:profile "{...}"
(error) MISCONF Redis is configured to save RDB snapshots, but is currently
not able to persist on disk. Commands that may modify the data set are
disabled, because this instance is configured to report errors during
writes if RDB snapshotting fails (stop-writes-on-bgsave-error option).
Please check the Redis logs for details about the RDB error.

127.0.0.1:6379> CONFIG GET stop-writes-on-bgsave-error
1) "stop-writes-on-bgsave-error"
2) "yes"

127.0.0.1:6379> INFO persistence
rdb_last_bgsave_status:err
rdb_changes_since_last_save:184203
```

Two decisions come out of this. Decide explicitly whether this instance is durable or disposable, and configure it to match rather than inheriting whatever the image shipped with. And if it is durable, then it is not a cache and the rest of this article applies to it only loosely, because losing it now costs data rather than CPU.

### The stampede when everything expires at once

Set a thousand keys with a 3600-second TTL during a deploy and they expire within milliseconds of each other an hour later. Every request that would have been a hit becomes a miss, every miss queries the database, and the database receives an hour of accumulated traffic in one second. The application was fine at 14:00 and fell over at 15:00 with no change deployed in between. Explaining that afterwards is its own kind of difficult.

Search interest in the term has roughly tripled over the past year, as of writing, and more teams appear to be meeting it. The mitigations are unexciting: add jitter so TTLs land in a range rather than on a point, let a single request recompute a value while the others serve the stale copy, or refresh hot keys in the background before they expire. Jitter alone removes most of the risk and costs one line.

javascript

```
// Every key expires at the same instant. This is the bug.
await redis.set(key, JSON.stringify(value), 'EX', 3600);

// Spread expiry across a ten-minute window instead.
const ttl = 3600 + Math.floor(Math.random() * 600);
await redis.set(key, JSON.stringify(value), 'EX', ttl);
```

### Keys that outlive the schema

The fourth failure arrives with a deploy. You add a field to a serialised object, ship it, and for the next hour the application reads cached entries written by the previous version, which do not have that field. Depending on the language this surfaces as a null, a type error, or a page rendering with something missing and no error at all.

This is one of two invalidation decisions worth making deliberately; the other is [whether a write deletes the cached key or overwrites it](https://runsite.app/blog/cache-aside-write-through-write-behind), which is where most stale-cache bugs actually come from. The remedy here is to put a version in the key rather than to remember to flush. A prefix such as `v3:user:1042` means a release that changes the shape of the object also changes the namespace, old entries are ignored and then expire on their own, and a rollback finds its own entries still valid. Flushing the whole instance on deploy is the alternative, and it trades a correctness bug for a stampede.

## Your cache holds personal data

Across both results pages measured for this article, eighteen organic results and one fully expanded AI Overview, not one mentions where the cache runs, who operates it, or what is legally inside it. The answer is less comfortable than the silence suggests.

A cache holding sessions holds session tokens, user identifiers, email addresses, roles and whatever else was convenient to keep on the session object. Under the GDPR that is personal data, and it does not become less so for being temporary or being called a cache. The same [residency questions that apply to your database](https://runsite.app/blog/gdpr-data-residency) apply here: which region the instance runs in, whether snapshots or replicas land somewhere else, and whether the connection between your application and the cache is encrypted. Check the last one today. The difference between an encrypted session store and an unencrypted one is a single character in a URL.

bash

```
# Plaintext. Sessions cross the network in the clear.
REDIS_URL=redis://default:password@cache.example.eu:6379

# TLS. One extra 's', and the port is usually the same 6379.
REDIS_URL=rediss://default:password@cache.example.eu:6379
```

The cache belongs in the processor list

Whoever runs your Redis instance is processing personal data on your behalf and needs [a data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) like any other subprocessor. Caches get missed in compliance reviews more than any other component, because the team classifies them as infrastructure rather than storage.

## What a cache costs

"Is Redis cache free" sits in the related searches for this topic with nobody in the top ten answering it, so here is the shape of the answer. Redis itself is free software you can run on a server you already pay for. A managed instance is priced by memory, and memory is the only dimension that matters until you reach failover and persistence.

Free tiers exist across European providers and they are small and time-limited, which is appropriate: a free cache is for proving the hypothesis, not for holding your sessions in production. The number to watch is not the monthly price but the clock, since a free database tier that expires after a fixed period is a different product from a free application tier that runs indefinitely.

The real cost question is sizing, and it is answered by the first failure in this article rather than by a pricing page. An undersized cache with `noeviction` set is an outage, and an undersized cache with eviction enabled is a hit rate falling quietly until the database is doing the work again. Both cost more than the next tier up. Start from the working set — how many keys, how large, and how long they need to live — and price that, rather than starting from the cheapest line and hoping.

If you are pricing a managed cache against running one yourself on a VPS, the arithmetic is the same one that applies to [a €5 server compared with a managed platform](https://runsite.app/blog/vps-vs-paas-cost): the advertised figure is not the invoice, and the maintenance is not free just because it is unbilled.

## How Runsite handles it

[Managed Redis for caches, sessions and job queues](https://runsite.app/services/redis) runs on Runsite as a toggle next to your application rather than as a separate account with a separate vendor. It is Redis 7+ with TLS on by default, as of writing, so the `rediss://` connection string above is the one you are handed rather than something you have to opt into.

| Plan     | Price  | Memory | Persistence                                            |
| -------- | ------ | ------ | ------------------------------------------------------ |
| Starter  | €5/mo  | 256 MB | RDB + AOF, automatic failover, TLS, private networking |
| Standard | €13/mo | 1 GB   | RDB + AOF, automatic failover, metrics                 |
| Plus     | €25/mo | 2 GB   | RDB + AOF, automatic failover                          |

Instance sizes go up to 10 GB. Prices as of writing.

On the residency question from earlier: instances run in the German region, and the keys, the sessions and any RDB or AOF files stay inside the EU. A signed GDPR data processing agreement is attached to every plan. The cache ends up in the same processor arrangement as the rest of the stack, rather than staying the component nobody wrote down. Most teams run it beside [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql) from €3 a month, which is the pairing the first half of this article assumes.

There is no free Redis instance any more. Redis starts at Starter, and the €5 of credit on a new account covers roughly a month of it if it is the only thing running, which is enough to find out whether the hit rate justifies the second copy before it costs anything. Configuration details, including the eviction policy on your instance, are in the [Runsite docs](https://docs.runsite.app).

## The short version

* A cache is a second copy of your data that is allowed to be wrong. Every failure below is that sentence playing out.
* Redis is a cache or a database depending on three settings you may not have chosen: expiry, eviction policy and persistence. There is no mode switch.
* Measure before caching. Of the six common slow-application symptoms in the table above, four have an answer that is cheaper than a cache, and two of those are an index and a connection pool.
* Cache repeated expensive reads, sessions shared across instances, counters and limits, and anything derived that can be recomputed. Losing it should cost CPU, not correctness.
* Do not cache values that must be right when read. A permission check behind a 300-second TTL is a security bug with a five-minute fuse.
* A single instance whose working set fits in process memory is faster without Redis. Redis wins at the second instance, or when the cache must survive a deploy.
* `maxmemory-policy` defaults to `noeviction` upstream, as of writing. A full instance then returns `OOM command not allowed when used memory > 'maxmemory'.` and refuses writes while reads keep working. Check `evicted_keys` against your memory ceiling.
* RDB snapshotting is on by default and `stop-writes-on-bgsave-error` is `yes`, so a full disk produces `MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk.` Decide durable or disposable, and configure it deliberately.
* Identical TTLs set during a deploy expire together and send an hour of traffic to the database in one second. Add jitter; it costs one line.
* Version your keys with a prefix so a schema change changes the namespace. Flushing on deploy trades a correctness bug for a stampede.
* A cache holding sessions holds personal data. It needs the same residency answer and the same processor agreement as your database, and it is the component most often missed in a compliance review.
* Price the working set, not the cheapest tier. An undersized cache is either an outage or a hit rate quietly falling until the database is doing the work again.

Related service

## Managed Redis on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed Redis](https://runsite.app/services/redis)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### When not to use Redis cache?

Four situations. When the data has to be correct at the moment it is read, such as a balance, a stock level or a permission check, because a cache is a window onto the past whose width you chose when you set the TTL. When writes outnumber reads, since a cache only pays for itself across repeated reads of the same key and otherwise adds write cost, invalidation cost and memory cost for almost no benefit. When the application runs as a single process and the working set fits in its own memory, where a plain in-process map is faster because it does not cross a socket; Redis starts winning at the second instance or when the cache has to survive a deploy. And when nobody has measured yet, because several of the most common slow-application symptoms have cheaper answers than caching, including a missing index and an unpooled database connection, and you cannot tell which one you have without looking.

### What is the difference between a cache and a Redis cache?

A cache is a pattern: keep a second, faster copy of data that is expensive to fetch or compute, and accept that it may be out of date. It can live in the memory of your application process, in the browser, on a CDN or in a dedicated server. A Redis cache is that pattern implemented in a separate process reached over the network, which buys two things a local cache cannot offer: every instance of your application sees the same entries, and the cache survives a restart or a deploy of the application. The cost is a network round trip on every read and one more component that can fail. The distinction matters most for a single-instance application, where the local map is genuinely faster, and it matters least once you run more than one container, at which point sharing the cache is the whole point.

### Is Redis faster than RAM?

No, and it cannot be, because Redis stores its data in RAM. The question is usually really asking whether a Redis lookup beats reading a value already held in your own application's memory, and there the answer is also no: an in-process map is a memory access, while a Redis read is a memory access on another machine plus serialisation, a network round trip and deserialisation. Redis reads are typically sub-millisecond, which is fast in absolute terms and slow next to a local variable. What Redis offers instead is that the memory is shared between every instance of your application and outlives any one of them. That trade is the reason to use it, rather than raw speed against local memory.

### What is Redis cache used for?

In practice, four things. Caching repeated expensive reads such as a product catalogue, a pricing table, a rendered page fragment or an aggregate that takes seconds to compute, where what matters is how often the value is read against how rarely it changes. Storing user sessions once an application runs on more than one instance, since in-process sessions break the moment a request lands on a different container, and automatic expiry is exactly what a session wants. Counters and limits, including rate limiting, quotas, view counts and feature-flag rollouts, which need atomic increments without a transaction against the primary database. And lightweight queues and Pub/Sub for background jobs and real-time features. The common thread is that losing the data costs CPU or a re-login rather than correctness.

Keep reading

---

# Cache-Aside vs Write-Through vs Write-Behind Caching

> Six patterns, two questions: who fills the cache and who writes to the database. Why cache-aside deletes rather than updates, and what write-behind costs.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Cache-Aside, Write-Through, Write-Behind: Choosing a Caching Pattern

CachingAugust 24, 202613 min read

# Cache-Aside, Write-Through, Write-Behind: Choosing a Caching Pattern

Six names for what is really two decisions. The axes people conflate, the race that makes cache-aside delete instead of update, and why every canonical write-behind implementation is deprecated.

## The short version

Six names, two decisions: who talks to the database, and whether the write reaches the cache. Cache-aside is the default, and its write path must be a delete rather than an update, because updating races. Write-through buys consistency on every write; write-behind buys speed and can lose acknowledged data, which is why its canonical implementations are deprecated.

[Teo Marquardt](https://runsite.app/about#author)

Six names get handed out for what is really two decisions, and the names are most of the reason this topic feels harder than it is. Cache-aside, read-through, write-through, write-behind, write-around, refresh-ahead. Presented as a list, they look like six competing options you have to rank against each other.

They do not compete. They answer two independent questions: how a value gets into the cache when it is missing, and how a write gets to the database. Separate those and the choice mostly makes itself.

This article stays at the level of the decision. Whether a cache is worth adding at all is [a different question with its own answer](https://runsite.app/blog/when-to-use-redis-cache), which engine you run it on is [a third](https://runsite.app/blog/redis-vs-memcached), and language-specific implementation belongs in [the Runsite docs](https://docs.runsite.app). What follows is how to pick, and what each choice costs you on the day it goes wrong.

## Two axes, not one list

The read axis asks who handles a cache miss. Either your application notices the miss and goes to the database, or the cache does that for you. That is the entire difference between cache-aside and read-through.

The write axis asks who puts the new value in the database, and when. Your application can do it and then clear the cached copy, the cache can do it synchronously before your write returns, or the cache can do it later on its own schedule.

| Pattern       | Who handles a read miss                       | How a write reaches the database                    |
| ------------- | --------------------------------------------- | --------------------------------------------------- |
| Cache-aside   | Your application                              | Your application writes, then deletes the key       |
| Read-through  | The cache                                     | Not defined. Pair it with a write pattern           |
| Refresh-ahead | The cache, in the background, before expiry   | Not defined. Pair it with a write pattern           |
| Write-through | Not defined. Usually paired with read-through | The cache, synchronously, before your write returns |
| Write-behind  | Not defined. Usually paired with read-through | The cache, asynchronously, after your write returns |
| Write-around  | Your application                              | Your application writes. The cache is not touched   |

Three of the six say nothing about writes and three say nothing about reads. They combine along the two axes rather than replacing one another.

Most systems in production are cache-aside on both axes, and most pages ranking for these terms present all six as a flat menu. The [Azure Architecture Center entry for cache-aside](https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside), the top organic result in Germany as of writing, is careful about the distinction. Several of the pages beneath it are not.

## Cache-aside: the default, and the line people get wrong

Cache-aside puts your application in charge of both axes. Read: ask the cache; on a miss, query the database and store the result. Write: update the database, then remove the key. The [AWS whitepaper on database caching strategies](https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/caching-patterns.html) calls the read half lazy loading, which is where most people first meet the alternative name.

It earns its position as the default. Any key-value store will do, because the store never has to know anything about your schema, and you decide key by key what is worth keeping. It also degrades honestly: when the cache is unavailable every read becomes a miss, and the database carries the load it used to carry before anyone added a cache.

One consequence is worth naming early: the first read of any key is a miss by definition, and on a container that has just been rebuilt, every key is a miss at once. That is [the cold start problem wearing a different hat](https://runsite.app/blog/cold-starts). A cache does not warm itself, and the pattern you choose does not change that.

### Why the write path is a delete, not an update

The tempting version of the write path is: update the database, then write the same value into the cache. One round trip saved, and the next reader arrives to a warm entry. It is also a race, and the loser stays lost.

Two concurrent operations, one slow read and one fast write:

text

```
T1  reader   GET user:42                        -> miss
T2  reader   SELECT ... WHERE id = 42           -> reads "Anna"
T3  writer   UPDATE ... SET name = 'Bea' WHERE id = 42
T4  writer   SET user:42 "Bea"                  -> cache is correct
T5  reader   SET user:42 "Anna"                 -> the stale read wins
```

The reader fetched its value before the update and stored it afterwards. The cache now holds `Anna`, the database holds `Bea`, and nothing in the system will notice until the key expires. That is not a millisecond of skew. That is the full width of your TTL serving a value which was already wrong at the moment it was written.

Deleting the key instead of setting it does not eliminate the race, and anyone who says it does has not drawn the diagram: a reader can still fill the cache after the delete with a value it read before the update. What changes is how long the damage lasts.

* With `SET` on write, two concurrent writers can also collide, and the writer whose database update landed first may be the one whose cache write lands last. The cache then holds a value that neither writer intended, for a full TTL, and the next write can produce the same outcome again.
* With `DEL` on write, the exposed window is the gap between one reader's database query and its cache fill, which is typically microseconds. Every subsequent write clears the entry again rather than replacing it with another guess.

The narrower window is the whole argument. If your data cannot tolerate even that, the answer is not a different pattern from this list. It is a lock around the fill, a version number inside the key, or a decision not to cache that value at all.

### Database first, cache second

Order matters as much as the operation. Delete the key first and write to the database second, and you have opened a window in which the cache is empty and the database still holds the old row. Any read landing in that window fetches the old value and caches it, and the stale entry is back for a full TTL. You created that window on purpose, and it is wider than the one you were trying to avoid.

The invalidation nobody retries

If the `DEL` fails because the cache is briefly unreachable, the entry stays stale and nothing comes back for it. Most implementations swallow that error, on the reasoning that a cache failure should never fail a request. That reasoning is right about the response and wrong about the logging: a failed invalidation is a correctness event, and on data that matters it deserves a retry rather than a shrug.

### Cache-aside vs read-through

The difference is one line of ownership. Under cache-aside your application handles the miss. Under read-through the cache handles it, and your code calls the cache and receives a value without knowing whether it came from memory or from a database query issued behind the cache.

Read-through is the cleaner arrangement where you can have it, because the fill logic exists in exactly one place instead of at every call site. The catch is in what "the cache handles it" actually requires.

## Read-through: what it actually requires

For a cache to fill itself on a miss, it has to reach your database, hold credentials for it, and know how to turn a key into a query. Key-value stores do not do this. It is a feature of caching products built around a loader interface, which is also where the terminology comes from.

Hazelcast, NCache, Oracle Coherence and Redisson all provide one, and all four expect you to implement a `CacheLoader` or its local equivalent. They are data grids or grid clients, sold on exactly this: the cache is a layer that owns access to the store behind it.

Plain Redis has no such interface. Neither does Memcached. What teams mean when they say they use read-through with Redis is almost always a helper function:

javascript

```
async function getUser(id) {
  const key = `user:v2:${id}`;

  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const row = await db.users.findById(id);
  await redis.set(key, JSON.stringify(row), { EX: 300 });
  return row;
}
```

That is cache-aside with the miss handling moved into one function. It is a good idea and you should do it. It is not read-through, and the distinction stops being pedantic the moment somebody goes looking for the configuration option that would enable a feature which does not exist.

## Write-through: consistency you pay for on every write

Write-through inverts the write path. Your application writes to the cache, the cache writes to the database, and the call does not return until both have succeeded.

What you buy is a cache that is never stale. Every write passes through it, so there is no invalidation to forget and no window to reason about. What you pay is a database round trip added to every write, a second component that can fail inside the write path, and a cache steadily filling with data nobody has asked to read.

That last cost is the one people discover late. Write-through populates the cache with everything written, whether or not it will ever be read again. On a write-heavy table with a long tail, an audit log or an events table, you are spending memory on rows no request will want, and evicting the rows that would have been hits. The hit rate falls while the bill stays the same.

Write-through is worth its cost in one shape of workload: the value must never be read stale, and it is written rarely enough that the extra hop is invisible. A settings table read on every request and updated twice a week fits exactly. An orders table is the opposite shape and will punish you for it.

### Write-through cache Redis: what plain Redis can and cannot do

The same limitation applies for the same reason. Redis will not write to PostgreSQL on your behalf. A write-through cache on managed Redis means your application writes to both, in order, and treats the pair as a single operation:

javascript

```
async function updateUser(id, patch) {
  // Source of truth first. If this throws, nothing else has happened.
  const row = await db.users.update(id, patch);

  await redis.set(`user:v2:${id}`, JSON.stringify(row), { EX: 300 });
  return row;
}
```

Two things are true about those five lines and both get missed. The pair is not atomic: the process can die between the statements, leaving the cache behind the database, which is the safe direction and still wrong. And reversing the order to write the cache first means a failed database write leaves the cache holding a value that was never committed, which is the unsafe direction and produces a phantom that survives until the TTL.

Redis transactions do not rescue this. `MULTI` and `EXEC` make a group of Redis commands atomic with respect to other Redis clients, and PostgreSQL is not inside that group.

A widely deployed example of the safe order is Django's `cached_db` session backend, which its documentation describes as a write-through cache: session writes go to the database and then to the cache, and reads fall back to the database on a miss. [Whether that hybrid beats Redis or the database alone for sessions](https://runsite.app/blog/redis-vs-database-for-sessions) comes down to expiry and revocation more than to speed.

Not this write-through

Around half the long tail for the phrase `write through cache` is about disk controllers rather than applications: `assuming drive cache: write through`, `intel rst cache mode write through`, `raid 5 cache policy write back vs write through`. Those describe whether a controller acknowledges a write before or after it reaches the disk. The idea has the same shape and none of the settings transfer. If a result mentions a battery-backed write cache, you have landed in the storage article.

## Write-behind: the fastest writes and the acknowledged data you can lose

Write-behind, also called write-back, is write-through with the second half made asynchronous. Your application writes to the cache, the cache acknowledges immediately, and the database is updated some time later, usually in batches.

The performance case is real and it is large. Writes return at cache speed. A thousand updates to the same counter collapse into one database write. A burst that would have queued behind the database's write throughput is absorbed by memory instead.

The cost fits in one sentence: there is a window during which data your users have been told is saved exists only in the cache.

### Write-through vs write-behind cache

|                                        | Write-through                           | Write-behind                                                     |
| -------------------------------------- | --------------------------------------- | ---------------------------------------------------------------- |
| Write latency                          | Cache plus database, on every write     | Cache only                                                       |
| Data loss if the cache dies            | None. The database already has it       | Everything inside the flush window                               |
| Database write volume                  | One per application write               | Batched, and repeated updates coalesce                           |
| Anything reading the database directly | Sees the write immediately              | Sees it after the flush. Reports, replicas and analytics all lag |
| Where a failure surfaces               | In the request. You can return an error | After you already returned success                               |

The last row decides it. Write-behind moves failure to a place where you can no longer tell the user about it.

### Write behind caching example

The shape is a buffer plus a flusher. Page view counts are the canonical fit, because losing a few seconds of them costs nothing anyone will notice:

javascript

```
// Application write path: cache only, returns immediately.
await redis.hIncrBy("pageviews", articleId, 1);
await redis.sAdd("pageviews:dirty", articleId);

// Flusher: a separate process, every few seconds.
const dirty = await redis.sPop("pageviews:dirty", 500);
if (dirty.length) {
  const counts = await redis.hmGet("pageviews", dirty);
  await db.pageviews.bulkUpsert(dirty, counts);
}
```

Read the second block as a catalogue of failure modes rather than as a recipe. `sPop` removes the identifiers before the database write has succeeded, so a crash between the two statements loses those updates for good. Moving the pop after the write risks writing twice, which is harmless for an idempotent upsert of an absolute count and harmful for an increment. Neither version survives the cache process dying with unflushed data, unless persistence is enabled and the last append actually reached disk.

Which is the real boundary of the pattern. Counters, view tallies and rate limit windows tolerate the trade. Put a payment through it and you have built a system that says yes and then quietly forgets.

### Why the canonical implementations are deprecated

Search for write-behind and the results page reads like a museum. As of writing, the first organic result in Germany is [Redis's own write-behind recipe](https://redis.io/docs/latest/operate/oss%5Fand%5Fstack/stack-with-enterprise/deprecated-features/gears-v1/python/recipes/write-behind/), at a URL containing `deprecated-features/gears-v1`. Third is [Oracle Coherence 3.4](https://docs.oracle.com/cd/E13924%5F01/coh.340/e13819/readthrough.htm), which documents read-through, write-through, write-behind and refresh-ahead together and is where the four names travel as a set. Eighth is IBM WebSphere eXtreme Scale 8.6.1\. The remainder are NCache, Redisson and Hazelcast, which are the same category of product.

Ranking did not do that on its own. Write-behind as a cache feature belongs to the era of the enterprise data grid, where the grid sat in front of the database and owned the write path by design. Outside that architecture the thing you want already has a different name: a queue, an outbox table, or a stream consumer. The mechanics are identical, in that the write is accepted fast and applied to the database later. The difference is that a queue is built to survive a restart, and a cache is built to be thrown away.

So if you find yourself designing write-behind on top of a cache, the useful question is whether you are reinventing a job queue with weaker durability guarantees. Sometimes the answer is no, and a counter flushed every ten seconds is exactly the right amount of machinery. Often the answer is yes.

### Refresh-ahead cache

Refresh-ahead is the read-path counterpart to write-behind: the cache re-fetches a value shortly before its TTL expires, so a popular key is never actually cold. Coherence documents it beside the other three, which is where most people encounter the name.

It solves the latency spike a hot key produces every time it expires, and it introduces a smaller problem in exchange, because refreshing keys nobody will ask for again burns database queries in the background indefinitely. Apply it to keys with a measured, steady read rate and to nothing else.

On plain Redis you approximate it in the application: read the remaining lifetime with `TTL key`, and when it drops below a threshold, refresh outside the request path. Randomise the threshold per key and the same mechanism doubles as stampede protection, which the [pillar covers alongside the other failures that only appear under load](https://runsite.app/blog/when-to-use-redis-cache).

## Write-around, and the pattern with no name

Write-around is the simplest write path available: write to the database and leave the cache alone. No update, no invalidation. The stale entry expires on its own schedule.

It is correct when the TTL is short enough that the staleness is acceptable and writes are rare enough that most entries would never have been invalidated anyway. It is also what a considerable number of production systems are doing right now, whether or not anybody chose it, because the invalidation call was never written in the first place.

The variant that appears on none of these lists is the one where you skip cache writes entirely and lean on a deliberately short TTL. It sounds lazy and it is frequently the right answer: a five-second TTL on a dashboard aggregate removes almost all of the database load for a page that refreshes every second, and it has no invalidation logic to get wrong. Correctness you do not have to implement is the cheapest correctness there is.

## Choosing a pattern

Read the left column as a situation rather than a product tier. Read paths and write paths still combine freely.

| If this describes your data                                        | Use                                           | Because                                                              |
| ------------------------------------------------------------------ | --------------------------------------------- | -------------------------------------------------------------------- |
| Reads dominate and a few seconds of staleness is acceptable        | Cache-aside with delete on write              | The default. Nothing else buys enough to justify its cost            |
| Reads dominate and the value must never be read stale              | Write-through, or do not cache it             | A TTL is a decision about how wrong you are prepared to be           |
| Writes dominate                                                    | Cache derived reads only, not the write path  | A cache on a write-heavy path adds cost per write and returns little |
| It is a counter or a tally, and losing a few seconds is survivable | Write-behind, or a queue                      | The one place write-behind's trade is clearly worth taking           |
| Writes are rare and a short TTL is acceptable                      | Write-around                                  | The invalidation you skip is invalidation you cannot get wrong       |
| One key is read constantly and is expensive to recompute           | Refresh-ahead, or probabilistic early refresh | Cheaper than the stampede it prevents                                |

Six situations, and cache-aside is the honest answer to more of them than the length of this table suggests.

Two questions sit outside the table and both get asked at this point. No pattern here decides whether caching is the right answer to a slow application; that comes first and [has its own decision frame](https://runsite.app/blog/when-to-use-redis-cache). And if writes are slow because the database has run out of connections rather than out of throughput, the fix is [a connection pooler in front of PostgreSQL](https://runsite.app/blog/pgbouncer-connection-pooling), which no caching pattern substitutes for. A third question arrives later, once the cache is full: the pattern you chose decides which keys are worth keeping, and [the eviction policy decides which ones actually survive](https://runsite.app/blog/redis-oom-command-not-allowed).

## What each pattern does to your GDPR paperwork

This section is absent from every result on both of the pages that rank for these terms, as of writing, and it is not a technicality. A cache holding personal data is a place where personal data is stored, with the residency, retention and processor obligations that follow from that fact.

The write pattern changes the shape of the obligation in a way that is easy to miss:

* **Cache-aside and write-around.** The cache holds a copy whose original lives in the database. Deleting the row does not delete the copy, so an erasure request has to name the cache explicitly. Otherwise the TTL is your retention policy, chosen by accident.
* **Write-through.** Everything above, plus the cache now receives every write, including columns you would never have chosen to cache. If a table has a field you would not put in a log file, write-through puts it in the cache.
* **Write-behind.** The strongest version. For the length of the flush window, the only copy of a committed write is in the cache. If your database is in the EU and your cache is not, then for those seconds neither is your data, and a processing record naming only the database is inaccurate rather than incomplete.

None of this is hard to satisfy. It needs the cache to sit in the same jurisdiction as the database, under the same processor agreement, named in the same documents. It gets skipped because the cache is the component teams forget they are running. [Where your data physically sits, and why that is the question a regulator actually asks](https://runsite.app/blog/gdpr-data-residency), applies to Redis exactly as it applies to PostgreSQL.

## How Runsite handles it

[Managed Redis for caches, sessions and job queues](https://runsite.app/services/redis) runs as a toggle beside your application rather than as a separate account with a separate vendor. It is Redis 7+ with TLS on by default as of writing, so the string you are handed is `rediss://` rather than something you opt into afterwards.

On the patterns above the honest summary is that you get a plain, fast, correctly configured Redis, and the patterns are yours to implement. There is no `CacheLoader` interface, because there is no Redis with one. What the platform does decide is the part that determines whether write-behind is defensible at all.

* **Persistence.** RDB snapshots plus an append-only file on every plan, from the €5 Starter up, as of writing. That matters here because write-behind on an instance with no append log loses the flush window on every restart, planned or otherwise.
* **Failover.** Automatic on every plan, so a node failure does not quietly empty a buffer that was about to be flushed.
* **Location.** Instances run in the German region. Keys, sessions and any RDB or AOF files stay inside the EU, and the signed data processing agreement attached to every plan covers the cache and the database as one arrangement rather than two.

The [full plan breakdown sits in the pillar](https://runsite.app/blog/when-to-use-redis-cache) rather than being repeated here, and most teams run the cache next to [managed PostgreSQL hosted in the EU](https://runsite.app/services/postgresql). A Starter instance on the €5 of credit every new account starts with is enough to run a real pattern against real traffic and find out whether the hit rate justifies the second copy. Choose the pattern first and size the instance second; doing it the other way round is how people end up running write-behind in a memory budget too small to hold the buffer.

## The short version

* Six names, two questions. How a value gets into the cache on a miss, and how a write reaches the database. The patterns combine along those axes rather than competing.
* Cache-aside is the default and is right for most workloads. Read: check the cache, miss, query, fill. Write: update the database, then delete the key.
* Delete the key on write rather than updating it. Updating lets a slow concurrent read leave a stale value in place for a full TTL; deleting narrows the exposure to the gap between one reader's query and its fill.
* Database first, cache second. Reversing the order creates a wider window than the one you were avoiding.
* Read-through and write-through need a cache that can reach your database. Plain Redis and Memcached cannot. What gets called read-through on Redis is cache-aside inside a helper function, which is fine as long as everyone knows that is what it is.
* Write-through never serves a stale value and adds a database round trip to every write. Worth it for values read constantly and written rarely, such as settings and feature flags.
* Write-behind returns writes at cache speed and puts committed data somewhere that can lose it. Fine for counters and tallies. For anything a customer will ask about later, use a queue, which is the same idea built to survive a restart.
* The canonical write-behind implementations are enterprise data grids. As of writing, Redis's own write-behind page sits under `deprecated-features/gears-v1`, and the results above and below it are Oracle Coherence 3.4 and WebSphere eXtreme Scale 8.6.1.
* `write through` also names a RAID controller setting, and roughly half the search traffic for the phrase is about disk caches. None of those settings transfer.
* Every pattern puts a copy of your data in the cache, and write-behind puts the only copy there for the length of the flush window. The cache needs the same residency answer and the same processor agreement as the database.

Related service

## Managed Redis on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed Redis](https://runsite.app/services/redis)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is a cache-aside pattern?

Cache-aside, also known as lazy loading, is a caching pattern in which the application code, rather than the cache, coordinates between the cache and the database. On a read, the application asks the cache for a key; if the value is present it is returned immediately, and if it is missing the application queries the database, stores the result in the cache with a time to live, and returns it. On a write, the application updates the database and then deletes the cached key rather than overwriting it, so that the next read repopulates the entry from the current state of the database. It is the most widely used caching pattern because it works with any plain key-value store, it gives the application full control over what is cached, and it degrades safely: if the cache becomes unavailable, every read turns into a miss and the database serves the traffic it would have served without a cache.

### How does the cache-aside pattern work in Redis?

In Redis the pattern is three commands and one rule. A read issues \`GET key\`; a nil reply is a miss, at which point the application queries the database and calls \`SET key value EX <seconds>\` to store the result with an expiry. A write updates the database first and then issues \`DEL key\`, so the stale entry disappears and the next reader rebuilds it. The rule is that the delete follows the database write rather than preceding it, because deleting first opens a window in which a concurrent read caches the old row again. Redis provides no built-in loader, so the miss handling lives in your application code; wrapping it in a single get-or-fetch helper is good practice, but it is still cache-aside rather than read-through. Two additions are worth making from the start: put a version prefix in the key, such as \`user:v2:42\`, so a schema change moves the whole namespace, and randomise the TTL by a few percent so that keys written together do not expire together.

### What is LRU and TTL?

They answer different questions and are frequently confused. TTL, or time to live, is an expiry you set on an individual key: after the specified number of seconds the key is removed whether or not memory is under pressure, and it is how you decide how stale a cached value is allowed to become. LRU, or least recently used, is an eviction policy that applies when the cache runs out of memory: Redis drops the keys that have gone longest without being accessed to make room for new ones, regardless of any TTL those keys carried. Redis approximates rather than implements true LRU, sampling a small number of candidate keys per eviction instead of maintaining a global ordering, which is cheaper and close enough in practice. So TTL is about correctness and freshness, and LRU is about capacity. A key can disappear for either reason. The important detail is that if no eviction policy is configured, a full cache does not evict at all; upstream Redis defaults \`maxmemory-policy\` to \`noeviction\`, which refuses new writes instead of making room.

### What are the three types of cache?

The phrase means two different things depending on who is asking. In computer architecture the three types are the CPU cache levels: L1, L2 and L3, which sit between the processor and main memory and differ in size and access latency. In application architecture, which is the subject of this article, the useful three-way split is by location: an in-process cache held in your application's own memory, which is fastest but private to a single instance and lost on every restart; a distributed cache such as Redis or Memcached, reached over the network and shared by every instance, which survives deploys and is the reason most teams run one; and a client or edge cache, meaning browser caching and CDN caching, which keeps the request from reaching your servers at all. The three are complementary, and a well-tuned system usually runs all of them at once.

Keep reading

---

# Redis vs Memcached: Which Cache to Choose in 2026

> Memcached is faster at one thing and simpler at everything. What changed in 2026: Redis went AGPLv3, Valkey took the BSD licence, and demand fell 46%.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Redis vs Memcached: When the Simpler Cache Is Still the Right One

CachingAugust 27, 202612 min read

# Redis vs Memcached: When the Simpler Cache Is Still the Right One

The comparison everyone links to is a Stack Overflow question closed as opinion-based fourteen years ago. What has changed since then is not the feature list. It is the licence, the fork, and a demand curve falling 46% a year.

## The short version

The comparison everyone links to is a Stack Overflow question closed fourteen years ago. What changed since is the licence, the Valkey fork, and demand falling sharply year on year. Memcached is still the better fit for plain string keys, genuinely disposable data, and a workload heavy enough to saturate one large multi-core machine.

[Teo Marquardt](https://runsite.app/about#author)

Search for this comparison in Germany as of writing and the first organic result is a Stack Overflow question from 2012\. Under the title sits the site's own notice: "Closed. This question is opinion-based. It is not currently accepting answers." It was closed five years ago and locked after that. The most-linked answer to this question is one the community decided could not be answered, and Google still puts it first.

That is not a knock on the thread, which is a good thread. It is a note on how old the framing is. Almost every page ranking for this comparison argues the same axis — Redis has more data structures, Memcached is simpler — and almost all of them were written before the two things that now decide the question.

Those two things are a licence change and a demand curve. So this is about when to use Memcached in 2026 rather than about which engine has the longer feature list, and it starts from the assumption that you have already decided you want a cache. [Whether a cache is worth adding at all](https://runsite.app/blog/when-to-use-redis-cache) is a separate question with its own answer, and so is [which caching pattern you write it with](https://runsite.app/blog/cache-aside-write-through-write-behind).

## What actually changed since 2024

For roughly fifteen years the two projects sat still. Memcached was BSD-licensed and did one thing; Redis was BSD-licensed and did many. Then, in March 2024, Redis stopped being BSD-licensed. Later releases moved to a dual RSALv2 and SSPL arrangement, and in May 2025 AGPLv3 was added as a third option. None of those is an OSI-approved permissive licence in the sense the old comparisons assumed.

The clearest public statement of what that means comes from AWS, on its own comparison page, which puts it as: "Memcached is fully open source (BSD licensing) while Redis OSS 7.2 is the last fully open source version of Redis." A group of former contributors forked 7.2 into Valkey, which is stewarded by the Linux Foundation under a BSD licence.

| Engine                | Licence as of writing                        | Since                         | Who steers it                         |
| --------------------- | -------------------------------------------- | ----------------------------- | ------------------------------------- |
| Memcached             | BSD (3-clause)                               | unchanged since 2003          | a small independent maintainer group  |
| Redis 7.2 and earlier | BSD (3-clause)                               | unchanged                     | frozen; this is the last such release |
| Redis 7.4 and later   | RSALv2 / SSPL, with AGPLv3 added in May 2025 | March 2024                    | Redis Ltd.                            |
| Valkey                | BSD (3-clause)                               | forked from Redis 7.2 in 2024 | the Linux Foundation                  |

The licence column is the part of this comparison that moved. The feature columns did not.

For most teams reading this, none of that changes anything: running an AGPLv3 server as a cache behind your own application is not a distribution, and no obligation is triggered. It matters if you ship software to customers who run it themselves, if you resell a hosted service built on it, or if your legal review has a list of licences it will not sign off on. That last category is larger than it sounds, and it is the reason a Memcached decision made in 2026 sometimes has nothing to do with throughput.

## Is Memcached still relevant?

Yes, in a narrower band of work than it used to occupy, and the band keeps narrowing. It is worth being specific about how fast, because the answer is easy to measure and nobody seems to print it.

Measured against the German market in August 2026, monthly search volume for `memcached` fell 46% year over year, from 720 a month to 390\. The comparison queries fell further: `memcached vs redis` is down 55%, `redis vs memcached` down 50%, `php memcached` down 57%. Those are English-language queries measured inside the EU, so the absolute numbers are small, and the direction is what matters. Every phrase in this vocabulary is shrinking, and none of the competing pages mentions it.

Search volume measures attention, not deployments, and there is a lot of Memcached quietly running in production that nobody is googling. But the trend is consistent enough across unrelated phrasings that the honest summary is this: the argument is being settled by attrition, not by benchmark. New projects reach for Redis, or now Valkey, and Memcached holds the installations that already exist and the narrow set of workloads described below.

What this is not

This is not a claim that Memcached is dead or unmaintained. It receives releases, it is genuinely faster at one thing, and there are workloads where it is still the better answer. It is a claim that the number of such workloads is smaller every year, which is a different statement and a checkable one.

## The one thing Memcached is genuinely better at

Memcached is multithreaded. Redis executes commands on a single thread, with multithreaded I/O available for reading and writing sockets but not for running the commands themselves. Give both engines a machine with sixteen cores and Memcached will use them; Redis will use roughly one for command execution.

That is a real architectural difference and it produces a real result. It is also narrower than it first appears, because the way you scale Redis is not to give one process more cores — it is to run more processes. Shard across four Redis instances and you have four command threads, on hardware that is usually cheaper than the single large machine. On a managed platform this happens without you thinking about it, which is why a Redis vs Memcached performance comparison tends to look more decisive on a benchmark chart than on a bill.

### Which is faster, Redis or Memcached?

On raw GET and SET of small values, at high concurrency, on one large machine: Memcached, by a margin usually reported in the tens of percent. Two of the pages currently ranking for this comparison put numbers on it. A production benchmark published on Medium reports roughly 470,000 requests per second for Memcached against 410,000 for Redis on read-heavy tests, with average latency of 1.2 ms versus 1.8 ms. A site built specifically for this query summarises it as a "\~20-25% GET/SET edge at high concurrency" for Memcached, and adds that "Redis wins everything else."

Those are other people's measurements, quoted as such, and they should be read with the usual caution about benchmarks you did not run: the hardware, the value sizes, the client library and the pipelining settings all move these numbers more than the engine choice does. What they establish is the shape of the result, not the value. The shape is that Memcached wins the narrow contest and loses the wide one.

The practical question is whether the narrow contest is your contest. If your cache is at 20% CPU on one core, a 25% throughput edge buys you nothing you can spend. If you are saturating a single Redis process at hundreds of thousands of operations per second and you cannot shard, it is the whole argument. Most applications are in the first group and think they are in the second.

## What are the limitations of Memcached?

The simplicity is genuine, and the constraints that come with it are the kind you discover in production, not in the documentation. Five are worth knowing before you choose.

| Limit          | Default as of writing             | What happens when you hit it                                                                                |
| -------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Value size     | 1 MB per item, changed with \-I   | SERVER\_ERROR object too large for cache, and the write is rejected, silently in many clients               |
| Memory         | 64 MB, changed with \-m           | least-recently-used eviction inside the affected slab class, not across the whole cache                     |
| Replication    | none                              | losing a node loses that node's share of the cache; the client reshards and the database absorbs the misses |
| Persistence    | none                              | a restart empties the cache completely, every time                                                          |
| Authentication | none in the default configuration | anything that can reach the port can read and write every key                                               |

The value-size limit is the one that catches people. A 1 MB ceiling sounds generous until a serialised object crosses it under real data, at which point the store fails and, depending on the client, does so without raising anything you would notice. The failure looks like a cache that suddenly has a terrible hit rate for one class of key.

The memory behaviour is subtler. Memcached allocates memory in slab classes with fixed chunk sizes, and an item goes into the smallest chunk that will hold it, wasting the remainder. Historically, memory assigned to one slab class stayed there, so a workload whose value sizes shift over time could end up with plenty of free memory in the wrong class and evictions in the right one. Later versions added automatic slab rebalancing, but the underlying model is still fixed-size chunks, and it is the reason Memcached memory usage is harder to reason about than the flat number suggests.

### port 11211

Memcached listens on port 11211, historically on both TCP and UDP. The UDP listener is the reason the port has a reputation: in 2018 it was used for record-setting amplification attacks, because an unauthenticated UDP service that returns more data than it receives is exactly what a reflection attack needs. Modern builds disable UDP by default as of writing, but the default installed by a distribution package is worth checking rather than assuming.

bash

```
# Memcached: no auth in the default configuration, so binding matters
memcached -m 64 -I 1m -l 127.0.0.1 -p 11211 -U 0
#          |     |      |               |         |
#          |     |      |               |         disable the UDP listener
#          |     |      |               default TCP port
#          |     |      bind to loopback only
#          |     max item size (default 1 MB)
#          memory limit in MB (default 64)

# Redis on Runsite: TLS is on by default, the credential is in the URL
REDIS_URL=rediss://default:password@redis.runsite.app:6379
```

Memcached has supported TLS as a build-time option since 2019, but it is a flag you compile in, not a default, and many distribution packages ship without it. If you run Memcached across anything other than a private network, that is a detail to verify rather than assume.

## `memcache` vs `memcached`: not two products

A large share of the search traffic for this phrase is not asking about the server at all. In PHP, `memcache` and `memcached` are two different client extensions: the older `memcache`, and the newer `memcached`, which wraps the libmemcached library and supports more of the protocol. Both talk to the same Memcached server. Choosing between them is a client-library decision with nothing in it about Redis.

If you arrived from a hosting control panel

Shared hosting panels like cPanel and LiteSpeed, and the object-cache settings in WordPress, Nextcloud and Moodle, frequently offer Memcached and Redis as two checkboxes with no explanation. In that context the answer is nearly always Redis if the panel offers it, because the CMS object-cache plugins are better maintained against Redis and because the persistence and eviction behaviour is easier to reason about when something goes wrong. None of the architectural argument below applies; you are picking a backend for someone else's plugin.

## Redis vs Memcached vs Valkey

The two-way comparison is the one people search for and the three-way one is the one that describes 2026\. Valkey is a fork of Redis 7.2 maintained under the Linux Foundation, and AWS states plainly on its own page that "Valkey is fully open source (BSD licensing) and will always be open source as it's stewarded by the Linux Foundation."

For an application developer, the practical position as of writing is that Valkey is protocol-compatible with the Redis version it forked from, so existing clients connect to it without changes, and the commands you use in a cache are the same commands. The divergence is at the edges: newer modules, newer data types, newer clustering behaviour. It widens with every release on both sides. Anything you rely on beyond core commands is worth checking against the specific version rather than against the general claim of compatibility.

Which splits the question in two. First: do you want a rich in-memory data structure server or a plain key-value cache? Then, if you want the first: on Redis Ltd.'s licence or the Linux Foundation's? They are separate decisions, and most of the pages ranking for this comparison still fold them into one.

## A cache with no persistence is not a GDPR exemption

It is tempting to treat the ephemeral cache as the component that does not count. It holds no durable copy, it empties on restart, and it is the last thing anyone adds to a processing record. Under the GDPR that reasoning does not hold: the obligation attaches to processing personal data, not to storing it durably, and a cache holding session tokens, user profiles or rendered pages containing someone's name is processing personal data for as long as the key lives.

What changes between the two engines is the shape of the risk, not whether it exists. The two also differ in what happens when memory runs out, which is a decision Memcached makes for you and Redis hands to you: [the eviction policy and the OOM error it produces](https://runsite.app/blog/redis-oom-command-not-allowed) is the Redis-side version of this.

* **Memcached.** No persistence means no on-disk copy to secure, restore or delete, which genuinely removes one class of exposure. It also means no audit trail: after an incident there is nothing to examine, and no default authentication means the access-control story is entirely about the network.
* **Redis or Valkey.** Persistence is optional, and if you enable RDB or AOF you have created a durable file containing personal data, which needs the same residency answer, the same retention rule and the same deletion path as your database. Access control exists in the engine, which is one fewer thing resting on the firewall.
* **Both.** The cache needs to sit in the same jurisdiction as the database it caches, under the same processor agreement, named in the same documents. A processing record that lists only the database describes a system you are not running.

None of that is hard to satisfy, and it is skipped constantly because the cache is the component teams forget they are running. It is also the one thing none of the pages currently ranking for this comparison mentions — across three results pages and twenty-six organic results, not one says where the cache physically runs or who the processor for it is. [Where your data actually sits, and why that is the question a regulator asks first](https://runsite.app/blog/gdpr-data-residency), applies to a cache exactly as it applies to a database.

## When to use Memcached: three questions, not a feature table

Feature tables are how this comparison is usually presented and they are close to useless for deciding, because they list capabilities when what you have is requirements. Three questions get you there faster.

1. **Do you need anything other than string keys and string values?** Counters, queues, rate limiters, leaderboards, sets, sorted sets, streams, pub/sub. If yes, the decision is already made — Memcached does not have them, and reimplementing a rate limiter on top of a plain key-value store is a project, not a configuration.
2. **Does anything need to survive a restart?** The question is not whether losing the cache is acceptable, which is true by definition of a cache. It is whether losing it on every deploy is. A cold cache after each release means every deploy sends full traffic at your database. Memcached empties on every restart, with no option.
3. **Are you actually CPU-bound on one cache process?** Not theoretically, measured. If a single Redis process is saturated at high concurrency and sharding is genuinely unavailable to you, Memcached's multithreading is a real answer. If you have not measured this, you are not in this case.

If all three come out in Memcached's favour, meaning plain strings, throwaway data and a measured single-process ceiling, then it is the better tool, and it is a pleasure to operate precisely because there is so little of it. That combination is uncommon, and it gets less common every year. Everything else is Redis or Valkey, and the choice between those two is a licensing question rather than a technical one.

## How Runsite handles it

Worth saying plainly: Runsite runs [managed Redis hosting in the EU](https://runsite.app/services/redis) and does not offer Memcached. If your answers to the three questions above point at Memcached, this platform is not the one that serves that case, and the rest of this section will not be useful to you.

For everyone else, it is Redis 7+ as a toggle beside your application instead of a separate account with a separate vendor, with TLS on by default as of writing, so the string you are handed is `rediss://` rather than something you switch on afterwards. What matters against the comparison above is mostly what the plan gives you.

* **Persistence comes with every plan.** As of writing, every Redis plan from the €5 Starter up includes RDB snapshots and an append-only file, and the old free in-memory-only instance is no longer offered. The engine difference in question two above only counts in Redis's favour if you actually rely on data surviving a restart.
* **Sharding, not cores.** Scaling a cache means running more instances, not giving one instance more cores, so the multithreading argument mostly resolves into a sizing decision instead of an engine decision.
* **Location.** Instances run in the German region. Keys, sessions and any RDB or AOF files stay inside the EU, and the signed data processing agreement attached to every plan covers the cache and [the database it sits in front of](https://runsite.app/services/postgresql) as one arrangement, not two.

A Starter instance on the €5 of credit every new account starts with is enough to run real traffic against a real hit rate and find out whether the second copy of your data earns its place.

## The short version

* The top-ranked page for this comparison is a Stack Overflow question closed as opinion-based in 2021 and originally asked in 2012\. Most of the pages under it argue a feature axis that stopped being the deciding one in 2024.
* What changed is the licence. Redis 7.2 is the last BSD release; later versions are RSALv2/SSPL with AGPLv3 added in May 2025\. Valkey forked 7.2 and is BSD under the Linux Foundation.
* Memcached is genuinely faster at raw GET and SET at high concurrency, by roughly 20-25% in other people's published benchmarks, because it is multithreaded and Redis executes commands on one thread.
* That advantage mostly disappears when you shard, because more Redis processes means more command threads, usually on cheaper hardware than one large machine.
* Memcached's limits are 1 MB per value by default (`SERVER_ERROR object too large for cache`), 64 MB of memory by default, no replication, no persistence, and no authentication in the default configuration.
* Slab allocation means memory usage is harder to reason about than the flat number suggests: memory can be free in one slab class while another is evicting.
* Port 11211 carried UDP by default for years and was the vehicle for the 2018 amplification attacks. Modern builds disable UDP, but check instead of assuming.
* In PHP, `memcache` and `memcached` are two client extensions for the same server, not two products. If you arrived from a hosting control panel, that is the question you were asking.
* Three questions decide it: do you need anything beyond string keys and values, does anything need to survive a restart, and are you measurably CPU-bound on one cache process. All three have to favour Memcached for it to win.
* Neither engine exempts you from data residency. A cache holding session tokens is processing personal data, and it needs the same jurisdiction and the same processor agreement as the database it sits in front of.

Related service

## Managed Redis on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed Redis](https://runsite.app/services/redis)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is Memcached still relevant?

Yes, but in a narrowing band of work. It remains a good fit for a specific profile: plain string keys and values, data that is genuinely disposable on restart, and a workload heavy enough to saturate a single-threaded process on one large multi-core machine. Outside that profile the reasons to choose it have been eroding for years, and the search data reflects it — measured against the German market in August 2026, monthly volume for \`memcached\` was down 46% year over year, with the comparison queries down 50-55%. That is a proxy for attention rather than for installations, and there is a great deal of Memcached running quietly in production that nobody is searching for. The fair summary is that it is a maintained, capable, extremely simple cache that new projects rarely start with, and that the argument against it is not that it is worse at what it does but that what it does covers less ground than it used to.

### What are the limitations of Memcached?

Five, and all of them follow from the same design decision to keep the server as simple as possible. Values are capped at 1 MB by default, changed with the \`-I\` flag; exceed it and the write is rejected with \`SERVER\_ERROR object too large for cache\`, which many clients swallow without raising anything visible. Memory defaults to 64 MB and is allocated in slab classes with fixed chunk sizes, so memory can sit free in one class while another is evicting, which makes usage harder to reason about than a single number suggests. There is no replication: losing a node loses that node's share of the cache, the client reshards, and your database absorbs the misses. There is no persistence at all, so every restart, including every deploy, starts from an empty cache. And there is no authentication in the default configuration, meaning anything that can reach port 11211 can read and write every key. SASL exists, but it has to be enabled deliberately. None of these is a defect; each is the cost of a server with very little in it.

### When should you not use Redis?

Three situations, and only the first is common. If your legal or procurement process will not accept RSALv2, SSPL or AGPLv3, which applies mainly to teams that ship software customers run themselves or resell a hosted service built on it, then Redis 7.4 and later is off the table, and the answer is Valkey or Memcached rather than a workaround. If you have measured a single Redis process saturated at high concurrency and sharding is genuinely unavailable to you, Memcached's multithreading is a real answer to a real problem. And if you are reaching for Redis as a primary database rather than as a cache, the question changes entirely: durability guarantees, backup and restore, and query patterns become the criteria, and a relational database usually wins on all three. Notably, none of these is about speed. Redis is fast enough for effectively every application that is considering this comparison.

### Is Valkey compatible with Redis?

At the level most applications care about, yes. Valkey forked from Redis 7.2, so it speaks the same wire protocol and the same core commands, and existing Redis clients connect to it without code changes — the connection string, the GET and SET calls, the TTL handling and the data structures all behave as they did. The caveat is that compatibility is a snapshot rather than a promise. Both projects have shipped releases since the fork and both continue to add features independently, so the further you get from core commands, the less the general claim tells you and the more you need to check a specific version. Modules, newer data types and clustering behaviour are where divergence shows up first. As of writing, a straightforward application cache moves between the two without changes; anything depending on version-specific behaviour should be tested against the exact build you intend to run.

Keep reading

---

# MinIO Alternatives: Open Source Options

> MinIO is archived and read-only. Garage, SeaweedFS, RustFS and Ceph compared on what each costs to run, and when not to self-host at all.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. MinIO Alternatives: What to Run Now That the Repository Is Archived

StorageAugust 31, 202612 min read

# MinIO Alternatives: What to Run Now That the Repository Is Archived

The MinIO repository is archived and read-only. Garage, SeaweedFS, RustFS and Ceph compared on what each one actually costs to operate, and the question the rest of the internet skips, which is whether you should be running object storage yourself at all.

## The short version

The MinIO repository is archived and read-only, and AIStor Free is not the open-source version, so a decision has to be made either way. Garage is the small one, SeaweedFS handles very large numbers of small files, RustFS is the closest drop-in and the least finished, Ceph answers a bigger question than object storage.

[Teo Marquardt](https://runsite.app/about#author)

The most useful summary of this situation is a thread title on the Proxmox forum: "MinIO is dead... What's next?" On Reddit the same week, someone opened with "MinIO CE is on life support" and asked which of three replacements to pick. Both threads rank on the first page for this search as of writing, which tells you something about how much settled guidance exists.

So start with the part that is not a rumour. Open the repository and GitHub shows a banner above the code.

github.com/minio/minio, checked 31 August 2026

"This repository was archived by the owner on Apr 25, 2026\. It is now read-only."

The [README](https://github.com/minio/minio) opens with a line in capitals:

text

```
THIS REPOSITORY IS NO LONGER MAINTAINED.
```

That is the whole premise. This is not another licence-change post. The licence argument happened, and then the project stopped. What follows is what the four credible replacements are actually like to run, what each one costs you once you count past the download, and the question almost nobody on this search result page asks: whether you should be operating object storage yourself in the first place. If you want the mechanics of the S3 API itself before any of that, [that is a separate piece](https://runsite.app/blog/s3-compatible-object-storage).

## What happened, and what "maintenance mode" turned into

The sequence is short. In December 2025 the company behind the project announced it was in maintenance mode and would not accept new changes. On 25 April 2026, by the timestamp GitHub prints, the repository was archived. [LWN](https://lwn.net/Articles/1077739/), reviewing the fallout in June, dates the archiving to February. The discrepancy probably comes from separate repositories being closed at different times, so the banner is the safer thing to quote.

The code did not vanish. It is AGPLv3, it still builds, and nothing stops you running the last release for years. What ended is the part you were relying on without pricing it: someone else fixing the bugs, someone else shipping the security patches, someone else deciding what the S3 compatibility surface does next. An archived repository is a dependency with a maintainer of exactly zero.

### AIStor Free is not the open-source version

The archived README points readers at two products: AIStor Free and AIStor Enterprise. It is worth being precise about what that offer is, because the naming invites a misreading. AIStor Free is a no-cost tier of a proprietary product. It is not a community edition, and it is not the thing you had before under a different name.

If you were running MinIO because it was free, that path stays open. If you were running it because the source was yours to read, patch and keep, moving to AIStor Free hands you the same exposure you were trying to avoid: a vendor deciding, on their schedule, what the free tier is allowed to do. The search traffic suggests people worked this out on their own: interest in AIStor is climbing, and so is interest in forks and alternatives, at the same time.

## The question the search results skip: should you run object storage at all?

Look at who is answering this question. A thread in r/selfhosted. A homelab YouTube channel. XDA. The Proxmox forum. These are excellent sources and they share an assumption that is invisible until you name it: that running the storage yourself is a given, and the only open question is which binary.

For a homelab that assumption is correct. The hardware is already bought and running it is the point. For a team that installed MinIO three years ago as an S3-compatible backend for user uploads, it usually is not. That team did not want a distributed storage cluster. They wanted a bucket with an S3 API so the SDK they had already written against would work, and MinIO was the cheapest way to get one.

If that is your situation, the honest answer is that you may not need a replacement binary at all. You need a bucket. Point the same client at a managed S3-compatible endpoint, change two config values, and the migration is done. [The comparison of managed providers on price and egress](https://runsite.app/blog/s3-alternatives-no-egress-fees) is the branch to follow from there. The rest of this article is the other branch: you have a real reason to hold the data on your own disks, and you need to know what you are signing up for.

Reasons that genuinely justify self-hosting

Data that cannot leave a specific building or network. An air-gapped environment. Volumes large enough that per-gigabyte pricing stops making sense against owned hardware. An existing storage team with capacity to take on another service. If none of these describe you, the managed branch is not a compromise — it is the correct answer.

## MinIO alternatives: the open source options that are still maintained

Four projects come up repeatedly, and they are not interchangeable. Garage is built for small deployments spread across locations. SeaweedFS is built for enormous numbers of small objects. RustFS is the closest thing to a drop-in and the least finished. Ceph is the one that answers questions larger than object storage. Pick by which sentence describes you, not by which benchmark looks best.

## Garage: the small one, and it knows it

Garage, the S3 implementation hosted by the Deuxfleurs association in France, is the option that keeps coming up in European self-hosting circles, and the one that has been most explicit about what it is for. It is written in Rust, it has been going since 2020, and it ships what [the project](https://garagehq.deuxfleurs.fr/) calls "a single dependency-free binary that runs on all Linux distributions".

The design target is unusual and worth understanding, because it explains both the appeal and the limits. Garage is built to "run over the Internet across multiple datacenters", not across a fast local network in one rack. It uses conflict-free replicated data types so that nodes on flaky links converge without a coordinator, runs one daemon per host, and asks very little of the hardware. If you have three modest machines in three different places, this is the design that fits.

The cost of that focus is API surface. In LWN's review, Garage's S3 support is narrower than Ceph's: no object versioning, and Signature Version 4 only. Neither is exotic. Plenty of applications never touch versioning, and every current SDK signs with SigV4\. But both are the kind of thing you discover on a Friday rather than during evaluation. Check your application against the compatibility list before you commit, not after.

* Good fit: multi-site or geographically split storage, modest hardware, small operations team.
* Bad fit: you rely on object versioning, or on the long tail of S3 features a backup vendor might use.
* Licence: AGPL, as of writing.

## SeaweedFS: the one built for a very large number of small files

[SeaweedFS](https://github.com/seaweedfs/seaweedfs) is the oldest and busiest project in this set. It is written in Go, and its architecture comes from a different lineage than the rest. It follows the approach Facebook described for its photo storage, keeping file metadata in memory so that reading an object costs a single disk seek regardless of how many objects exist.

That is a real answer to a real problem. Storage systems that treat every object as a file on a filesystem degrade badly at tens of millions of small objects, and this one does not. S3 arrives as a gateway on top of that core rather than as the native interface, which is the thing to understand before choosing it: SeaweedFS is a distributed storage system that speaks S3, not an S3 server that happens to be distributed.

Practically, this makes it the least drop-in of the four. The concepts you learn are SeaweedFS concepts: volumes, masters, filers. They do not map onto what you knew about MinIO. Budget for that. In exchange you get the project in this list with the longest operational track record at scale.

## RustFS: the closest thing to a drop-in, and the least finished

RustFS is the project the AI summaries and the forum threads keep putting first, and it is easy to see why. Its [repository](https://github.com/rustfs/rustfs) tagline is a benchmark claim, "2.3x faster than MinIO for 4KB object payloads". It is Apache 2.0 licensed, it ships a management console, and it is openly positioned as the replacement people are looking for.

Take the benchmark line as what it is: a project's claim about itself, on a payload size it chose. It may well hold. It has not been independently reproduced in anything ranking on this search as of writing.

### Which is better, MinIO or RustFS?

For a new deployment, RustFS, because MinIO is archived and RustFS is not. That is nearly the whole comparison and it has little to do with throughput.

The caveat matters more than the answer. RustFS is young, and the community consensus in the threads Google is currently surfacing is that it is not yet recommended for large-scale production use. Distributed operation in particular is described as still maturing. Google's own summary of this search repeats that warning while listing the project first, which is an unusually honest thing for a generated answer to do. Single-node, non-critical, or a staging environment: reasonable today. The bucket holding your customers' uploads: not yet, unless you are prepared to be the one who finds the bugs.

## Ceph: the answer when the question is bigger than object storage

Ceph is the mature end of this list. It is stewarded by [the Ceph Foundation](https://ceph.io/) under the Linux Foundation, it has been in production at serious scale for over a decade, and its RADOS Gateway is the most complete S3 implementation here: bucket lifecycle rules, encryption, the features a backup product expects to find.

It is also the one where the phrase "just replace MinIO with it" does the most damage. Ceph is several coordinated daemon types (ceph-osd, ceph-mon, ceph-mgr and more) with their own hardware expectations and their own failure modes. Ubuntu's MicroCeph and similar packagings genuinely reduce the setup burden, and they do not reduce the operational one. You are running a storage cluster now.

The case for it is honest and narrow: if you also need block devices and a POSIX filesystem, or you are storing enough that a dedicated storage function already exists, Ceph is the only option here that answers all of those at once. If you needed a bucket, this is the most expensive way to get one.

## The table nobody prints: what each one costs to run

Every comparison of these projects compares the software. The software is free in all four cases, which is exactly why comparing it settles nothing. Here is the part that decides the bill.

|                                | Garage                               | SeaweedFS                 | RustFS                      | Ceph (RGW)             |
| ------------------------------ | ------------------------------------ | ------------------------- | --------------------------- | ---------------------- |
| Drop-in for MinIO              | Close, with gaps                     | No — different model      | Closest                     | No                     |
| Machines for a redundant setup | 3 modest                             | 3+                        | 3+ when distributed matures | 3+, sized for the role |
| Operational weight             | Low                                  | Medium                    | Low but immature            | High                   |
| S3 surface                     | Narrower — no versioning, SigV4 only | Gateway on a storage core | Growing                     | The most complete here |
| Production-ready today         | Yes, within its scope                | Yes                       | Not for critical data       | Yes                    |
| Licence, as of writing         | AGPL                                 | Apache 2.0                | Apache 2.0                  | LGPL family            |
| Who operates it                | You                                  | You                       | You                         | You                    |

The last row is the one that never appears in these comparisons, and it is the one that costs money.

Read that last row properly. Any of these run on one machine, and none of them should. A single node means a single disk controller, a single kernel, and a restore from backup as your recovery plan. Redundancy means at least three nodes, which means three sets of disks, replication traffic between them, and a person who knows what to do when one of them stops answering at two in the morning.

Price that in euros at European hardware rates and the shape becomes obvious. Three servers with real disks cost more per month than most teams' entire object storage bill was, before adding the engineer time to build the cluster, the periodic restore test that proves the backups work, and the on-call rotation that makes the redundancy mean anything. The licence was never the expensive part. Bandwidth is worth checking too, since self-hosting changes where [the egress arithmetic](https://runsite.app/blog/s3-egress-fees) lands rather than removing it.

## MinIO alternative for local development

This deserves its own answer, because it is a common reason MinIO was installed and the requirements are nothing like production. A development S3 endpoint needs to start fast, hold nothing valuable, and go away cleanly.

Nothing here has to be production-grade for that. Garage's single static binary is well suited to it, RustFS's alpha status stops mattering when the data is disposable, and lighter S3 mocks exist that are not object storage at all. The one thing worth keeping consistent between your laptop and production is the signing behaviour and path style, since that is what breaks first when the endpoint changes. If you have already written [presigned URL flows](https://runsite.app/blog/s3-presigned-urls), verify those against whichever local server you pick — they are the part most likely to differ.

## What self-hosting does to your GDPR position

This is missing from every result on the page as of writing, in a discussion about where user files live, and it changes the decision for a lot of European teams.

Self-hosting does not remove the obligations that come with holding personal data. It moves them onto you. Run your own object storage and you are the one answering for where the disks physically sit, for the security of the store, for breach notification when something goes wrong, and for the records of processing. There is no processor between you and the regulator, because you are it. Teams often reach for self-hosting specifically for compliance reasons and end up with more obligations than they started with, not fewer.

That is not an argument against it. It is an argument for counting it. If the reason you want the data on your own hardware is [data residency](https://runsite.app/blog/gdpr-data-residency), a managed provider operating inside the EU already gives you that, with a processor agreement and someone else's name on the security of the store. If the reason is that you do not want the data under a foreign legal regime, [that is a question about the operator, not the hardware](https://runsite.app/blog/eu-vs-us-hosting), and it is the column none of these comparisons contains.

## How Runsite handles it

Runsite provides [managed S3-compatible object storage on European infrastructure](https://runsite.app/services/s3-storage), with servers in Germany as of writing. The API is the one your code already targets, so migrating off MinIO is an endpoint and a set of credentials rather than a project.

The part that is relevant to this article is the last row of the table. There is no quorum for you to size, no replication to watch, and no page at two in the morning when a node stops answering. The operator of the store is a European company, which means the processor obligations stay with the provider instead of moving onto your team along with the binary. Implementation details (SDK configuration, endpoints, credentials) are in the [Runsite docs](https://docs.runsite.app).

## Short version

| If this is you                                                       | Do this                                                                           |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| You installed MinIO to get an S3 bucket for user uploads             | Move to a managed S3-compatible provider. You do not need a cluster.              |
| You need multi-site storage on modest hardware                       | Garage, after checking your app against its S3 feature list.                      |
| You have tens of millions of small objects                           | SeaweedFS, and budget time to learn its model.                                    |
| You want the closest thing to a drop-in and the data is not critical | RustFS, single-node, eyes open about its maturity.                                |
| You also need block and filesystem storage at scale                  | Ceph, with someone whose job is Ceph.                                             |
| You just need a local S3 endpoint for development                    | Any of them, or a lighter mock. Match the signing behaviour, not the feature set. |
| You moved to AIStor Free to stay free                                | Re-read why you left. That is a proprietary product's free tier.                  |

The archive banner is real and the project is not coming back, so a decision has to be made either way. Just make sure it is the decision in front of you. Most of the internet is answering "which binary replaces MinIO" when the question worth asking first is whether the binary was ever the thing you wanted.

Related service

## Object Storage on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Object Storage](https://runsite.app/services/s3-storage)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What happened with MinIO?

The company behind MinIO announced in December 2025 that the project was in maintenance mode and would not accept new changes. The repository was then archived. GitHub's banner reads "This repository was archived by the owner on Apr 25, 2026\. It is now read-only" — and the README now opens with "THIS REPOSITORY IS NO LONGER MAINTAINED." The code remains available under AGPLv3 and still runs; what ended is bug fixes, security patches and any further development. The README points users to AIStor, the company's proprietary product.

### Is MinIO no longer open source?

The archived source is still AGPLv3, so the code you already have stays open source and you can keep running and modifying it. But no one is maintaining it, and the successor the project recommends, AIStor and its free tier included, is proprietary rather than a community edition. In practice that means the open-source option is a frozen snapshot with no future releases, which is why most teams treat it as an exit rather than a licence question.

### Which is better, MinIO or RustFS?

For any new deployment, RustFS, on the simple ground that MinIO is archived and RustFS is actively developed. RustFS is Apache 2.0 licensed and positions itself as a drop-in replacement, claiming on its repository to be "2.3x faster than MinIO for 4KB object payloads". That is a self-reported benchmark on a payload size it chose. The important caveat is maturity: community discussion as of writing does not recommend it for large-scale production use, and its distributed mode is still maturing. Single-node or non-critical workloads are a reasonable fit; critical customer data is not, yet.

### Is Garage better than MinIO?

Garage is maintained and MinIO is not, which settles it for new deployments. On design they target different situations: Garage is built to run across multiple datacenters over ordinary internet links on modest hardware, using conflict-free replicated data types so nodes converge without a coordinator, and ships as a single dependency-free binary. Its S3 surface is narrower than MinIO's was: no object versioning, and Signature Version 4 only. Check your application's requirements against its compatibility list before migrating.

Keep reading

---

# PaaS vs IaaS: Where the Responsibility Line Falls

> IaaS and PaaS split the work, the security controls and the GDPR roles along three different lines. What each costs you, and when the raw server wins.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. PaaS vs IaaS: Where the Responsibility Line Actually Falls

DeploymentSeptember 7, 202613 min read

# PaaS vs IaaS: Where the Responsibility Line Actually Falls

Every diagram draws one line between you and your provider. There are three, they fall in different places, and the one that matters legally is not on any of the diagrams.

## The short version

Every diagram draws one line; there are three. The operations line decides who patches what. The security line runs somewhere else, and the shared-controls row is where both sides assume the other has it. The third line is legal — controller, processor, Article 28 — and it appears on none of the diagrams.

[Teo Marquardt](https://runsite.app/about#author)

Search for the difference between IaaS and PaaS and you will get the same picture from every result: a stack of horizontal layers, hardware at the bottom and your application at the top, with a line drawn across it. Above the line is yours. Below it is theirs. The picture is accurate and it is where almost every explanation stops.

Google's own generated answer for this comparison, as of writing, puts it in one sentence: with IaaS "you manage the operating system", and with PaaS the provider "takes care of servers, operating systems, and software updates". That is true. It also does not tell you what managing an operating system consists of, when any of it lands, or who a regulator will hold responsible when the data inside it leaks.

There are three lines, not one. The operational line decides whose calendar the work appears on. The security line runs somewhere else and has a category on it where both sides quietly assume the other one is covering it. The legal line, the one that determines who answers for personal data, is drawn by a completely different instrument and appears on none of the diagrams.

This article separates the three, lists what actually sits on each side, and gets to the part the comparison usually skips: the cases where the raw server is still the right answer, and what changes about all of it if you are operating in the EU.

## The layer cake tells you the shape, not the bill

The layer diagram is a good model of one thing: what exists. Hardware, hypervisor, operating system, runtime, application, data. Both service models contain the same layers and disagree only about where responsibility for them changes hands. As a map of the territory it is fine.

The trouble is that responsibility is not evenly distributed across the height of the stack. Two layers of the diagram might be twenty minutes of work a year between them, and one layer might be the reason someone was awake at three in the morning last March. A diagram that gives each layer the same visual weight tells you nothing about that, and every comparison that stops at the diagram inherits the problem.

So the useful question is not which layers you own. It is which jobs you have signed up for, how often each one arrives, and whether it arrives on a date you chose.

## Who manages what

That heading is lifted verbatim from Google's generated answer, where it appears as the subheading under both service models and is followed by one sentence each. Here is the longer version, written as a list of jobs rather than a list of layers. The pattern below is the common one across European providers as of writing; the details vary and yours are in your provider's documentation, not here.

| The job                                                 | On IaaS                                                                                                         | On PaaS                                                                                      | When it becomes visible                                                           |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Kernel and OS security updates                          | Yours. Either you follow advisories, or you enable unattended upgrades and accept reboots you did not schedule. | The provider's. The base image is rebuilt and your application is redeployed onto it.        | When a CVE is published, which is never on your calendar                          |
| TLS certificate renewal                                 | Yours: an ACME client, a timer, and a reload hook that has to actually fire.                                    | The provider's, and usually invisible.                                                       | Ninety days after the last successful renewal, if a silent failure went unnoticed |
| Runtime version upgrades                                | Yours, including whichever native extension refuses to build on the new minor version.                          | Shared. The platform publishes versions, you decide when to move.                            | When your version leaves upstream support and stops getting patches               |
| Log rotation and disk pressure                          | Yours. logrotate is configured by you and tested by the disk filling up.                                        | The provider's. Logs leave the instance.                                                     | At 100% disk, when writes start failing and the symptom looks like something else |
| Release orchestration and rollback                      | Yours to build: health checks, connection draining, keeping the previous version around.                        | The provider's, with the health gate configurable by you.                                    | During the first bad deploy, which is when you find out what you did not build    |
| Firewall rules and network exposure                     | Yours. A port opened during debugging stays open until a human closes it.                                       | Shared. The platform terminates public traffic, you decide what listens behind it.           | During a port scan, or shortly after one                                          |
| Backups, and more importantly restores                  | Yours to run and yours to test, and the second half is the one that gets skipped.                               | The provider's for managed data services; the correctness of what you stored is still yours. | The first time you genuinely need one                                             |
| Application code, dependencies, secrets, access control | Yours.                                                                                                          | Yours.                                                                                       | This row does not move, and no service model will move it                         |
| Physical security of the hardware                       | The provider's.                                                                                                 | The provider's.                                                                              | Ideally never                                                                     |

The same jobs exist under both models. What changes is whose calendar they land on, and whether the date is one you picked. Read your own provider's documentation for the exact split — this is the shape, not a contract.

Read down the IaaS column and the honest summary is that most of it is nothing most of the time. A well-configured server with unattended upgrades and a certbot timer can run for a year without a human decision, and people who say theirs has are telling the truth. One row is missing from the table on purpose, because it belongs to your application rather than to either party: the scheduled and background work you run yourself, where [the choice between a cron job, a worker and a queue](https://runsite.app/blog/cron-jobs-workers-queues) is yours under both models and the failure modes differ.

### The jobs that only appear when they fail

Look at the fourth column instead. Almost nothing in it is scheduled. A certificate renewal that failed silently surfaces ninety days later, at the moment browsers start refusing the connection. A disk fills at a rate set by your traffic, so it fills fastest on your best day. A kernel advisory is published when the researcher finishes, and the window between publication and exploitation is not yours to negotiate.

That is the real content of the operational line. You are not choosing between more work and less work, because the average across a year may genuinely be close on both sides. You are choosing whether these events arrive as calendar items or as interruptions. Some teams should take the interruptions, and there is a section further down about who they are.

Cost sits in a different article

None of this is priced here on purpose. If you want the arithmetic — what a €5.49 European server actually invoices at, and how many maintenance hours the gap to a managed plan buys — [the itemised VPS versus PaaS cost comparison](https://runsite.app/blog/vps-vs-paas-cost) does that job. This one is about who is responsible, which is a separate question that survives after the money question is settled.

## The security line does not run where the operations line runs

There is a second boundary, and the industry has a name for it. The shared responsibility model is the framework cloud providers use to state which security duties are theirs and which are yours. AWS published the first version of it in 2011, according to Datadog's account of its origin, and every major provider now publishes a variant.

These documents are worth reading, and worth reading as an answer to a different question from the one above. The operations line asks who does the work. The security line asks who is accountable if a control fails. Those produce different lists, and the gap between them is where incidents live.

### "Security of the Cloud" and "Security in the Cloud"

AWS splits its model into two phrases that differ by one preposition. Security of the Cloud is the provider's half: the data centres, the hardware, the hypervisor, the managed services themselves. Security in the Cloud is yours: your data, who can reach it, how it is encrypted, and how everything you configured is configured.

The preposition carries the whole distinction, which is either elegant or a warning sign depending on your mood. In practice it means a provider can be completely correct about their half while your half is wide open, and nothing in the model has been violated. A long run of the cloud data exposures that made the news came down to a storage bucket left readable by anyone, and on every provider's diagram that sits squarely in the customer column.

### Shared Controls: the row where both sides assume the other one has it

The models also contain a third category, and it is the one that repays attention. AWS calls it Shared Controls and defines it, in its own words as of writing, as controls "which apply to both the infrastructure layer and customer layers, but in completely separate contexts or perspectives".

What a shared control looks like in practice

Patch management is the standard example. The provider patches the infrastructure; you patch the guest OS and your application. Both statements are true, and neither of them tells you who patches the container base image on a platform that rebuilds it weekly but redeploys only on your push. That gap is small, real, and exactly the shape of thing nobody notices until an auditor asks for the evidence.

The value of the shared category is that it is honest about ambiguity existing. The risk is that shared reads as covered. If a control appears in that row and neither party has written down who does what in your specific setup, it is not shared; it is unassigned.

This is the single largest practical difference between the two models. On IaaS, the shared row is short, because almost everything above the hypervisor is unambiguously yours. On PaaS, the shared row is longer, because the platform genuinely does more, and every job it does creates a boundary where an assumption can hide. Fewer jobs on your list, more edges to check.

## The third line nobody draws: controller, processor, and Article 28

Here is the part missing from the pages that rank for either of these searches. We read the German first page for "paas vs iaas" and for "shared responsibility model" as of writing. Across both, the shared responsibility model is presented as a security framework and nothing else. The GDPR's own division of roles is not on any of them.

That matters because the GDPR draws its own line, and it does not follow the stack. Under the regulation, the controller is whoever determines the purposes and means of processing personal data, and the processor is whoever processes it on the controller's behalf. Those roles are decided by who is making decisions about the data, not by which layer of the diagram the data is sitting on.

The practical consequence is that moving from IaaS to PaaS does not change your role. You are the controller in both cases. What changes is how much of your stack has a processor attached to it, and what that processor has committed to in writing.

| Setup                                          | Who is the controller | Who is a processor, and for what                                          | What the contract has to cover                                                                                    |
| ---------------------------------------------- | --------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Your own hardware in your own building         | You                   | Nobody. There is no external party in the chain.                          | Internal policy and records; there is no Article 28 contract to sign                                              |
| IaaS: a virtual machine at a European provider | You                   | The provider, for the infrastructure beneath the hypervisor               | Infrastructure only. The OS, runtime, application and logs above it have no processor, because that role is yours |
| PaaS: a managed application platform           | You                   | The platform, for the layer it operates — build, runtime, routing, logs   | The platform layer, plus a named list of its own sub-processors and where they operate                            |
| PaaS with managed databases and storage        | You                   | The platform, extended to the database and object storage it runs for you | As above, extended to backups, retention and the physical location of both                                        |

Roles under the GDPR follow decision-making, not layers. This is the usual shape of the arrangement; the binding version is whatever your provider's data processing agreement actually says. Nothing here is legal advice.

Notice the second row. On a raw server, the layers you took on have no processor behind them, so there is no one to hold accountable for them except you. [Our comparison of self-hosted control planes against managed platforms](https://runsite.app/blog/coolify-vs-managed-paas) puts it in a sentence: your data processing agreement now covers the metal, not the platform. Plenty of teams run their own infrastructure and meet their obligations properly, so this is not an argument against the server. It is a line item that belongs in the comparison rather than in a footnote.

Going the other way has its own homework. Every managed layer you adopt is a processor you have to check, and its sub-processors are ones you inherit. [What an Article 28 data processing agreement has to commit each side to](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) is the detail behind that, and it is a short enough read to do before signing rather than after.

## "Is Azure an IaaS or PaaS?"

That question is quoted exactly as Google lists it, and it is one of the four things people most commonly ask alongside this comparison. The others in the block are "Is AWS SaaS or IaaS or PaaS?" and "Is Netflix IaaS, PaaS, or SaaS?".

All three have the same answer, which is that the question is aimed at the wrong noun. The service models classify services, not companies. Azure sells virtual machines, which are IaaS, and App Service, which is PaaS, and Microsoft 365, which is SaaS. Naming the vendor does not narrow anything down.

Netflix is the more interesting case, because the confusion there is about direction rather than granularity. Netflix consumes infrastructure as a customer and sells a finished application to viewers. From below it is an IaaS tenant; from above it is SaaS. Both descriptions are correct and they are descriptions of different relationships.

The distinction earns its keep the moment you try to use it. Ask which category a company belongs to and you get an argument. Ask which model a particular service is sold under, then read that service's terms, and you get the boundary you were looking for.

Why "pizza as a service" gets you halfway

The pizza analogy — made at home, take-and-bake, delivered, eaten in a restaurant — is the most-shared explanation of these models, and enough people search for it that it has its own query. It is genuinely good at the thing it is for: showing that the layers are the same and only the boundary moves. It has nothing to say about the parts that matter here, because nobody in the analogy gets paged at 03:00, and no one signs an Article 28 contract about the pizza.

## When the raw server is still the right answer

Plenty of the time. Everything above is a description of a trade, and there are situations where the trade goes the other way. On the responsibility question specifically, IaaS wins here:

* You need the boundary to sit below the operating system. Some contractual or regulatory regimes require you to control and evidence the full stack yourself. If you have to produce your own patch records for every layer, a platform that patches the base image for you has taken away the artefact you needed to show.
* The workload needs things a platform does not expose. Custom kernel modules, raw sockets, a licensed binary that checks the hardware it is running on, a GPU, or a process that has to hold connections open for hours. No amount of responsibility-shifting helps if the platform will not run the program.
* Your team already carries the operational line. If someone is already patching a fleet, the marginal host is close to free. The expensive server is the first one, and organisations that have crossed that threshold are paying a different price than the comparison assumes.
* You want the boundary unambiguous rather than convenient. On a virtual machine the shared-control row is nearly empty: above the hypervisor, everything is yours and there is nothing to negotiate. Some teams would rather own more and argue less, and that is a legitimate preference rather than a failure to optimise.

Where it goes wrong is the case in between. A small team, nobody whose actual job is operations, real customers, and a server configured during a week when someone had time. On that setup the operational line is technically owned and practically unattended, which is the worst of the two arrangements: you carry the responsibility and the work is not getting done.

## Choosing in the EU: the examples nobody in the results uses

The examples that keep recurring in these results are American. The IaaS side gets illustrated with EC2 and Azure Virtual Machines, the platform side with Heroku, Google App Engine and Elastic Beanstalk. Google's generated answer for the comparison names those five and no others as of writing, which is a little strange given that it is answering a German query and that several of the pages it cites are German.

On the IaaS side the European market is not short of options. Hetzner, Scaleway, OVHcloud, Exoscale and UpCloud all sell virtual machines from European data centres under European corporate entities. If you decide the boundary belongs below the operating system, you do not have to leave the continent to put it there.

The platform side is thinner, and that thinness is part of why this question so often gets answered with a virtual machine. Teams who want their data in the EU look at the platform market, recognise most of the names as US companies, and conclude that they have to pick between convenience and residency. The conclusion is older than the market it describes.

Two things are worth checking on any candidate, whichever model you land on. The first is that a region is not an operator: a company incorporated outside the EU that offers an eu-central region is still governed by the law of the place it is incorporated, and [what actually changes between an EU region and an EU provider](https://runsite.app/blog/gdpr-data-residency) is the useful version of that check. The second is the sub-processor list, because each managed layer you adopt brings its own, and the question of where your data physically rests is answered by the last name on that list rather than the first.

## How Runsite handles it

For the PaaS side of this comparison, stated as the same three lines. [Runsite runs managed web app hosting in the EU](https://runsite.app/services/web-services), with the operational line above the runtime: the base image, the operating system, the certificates and the release mechanism are ours, and your code, your dependencies, your secrets and your data are yours. The build runs from a webhook on push, releases are rolling behind a health check you configure, and a failed check returns to the last healthy version instead of publishing a broken one. The mechanics of that, if you want them, are in [how git push to deploy actually works](https://runsite.app/blog/git-push-to-deploy).

On the legal line: servers are in Germany as of writing, and a signed GDPR data processing agreement comes with every account. That is the specific thing the IaaS row of the table above cannot give you, because on a virtual machine the obligation for everything over the hypervisor has nowhere to sit except with you. Managed [PostgreSQL](https://runsite.app/services/postgresql), [Redis](https://runsite.app/services/redis) and [object storage](https://runsite.app/services/s3-storage) extend the same processor relationship to the data layer rather than leaving it on your side of the line.

Being fair about the trade: everything in the shared-controls discussion above applies to us as much as to anyone. Adopting a platform means adopting its boundaries, and the right response is to read the agreement rather than take the summary. If the four conditions in the IaaS section describe your situation, a European virtual machine is the better answer and we would rather you took it. Setup details are in the [Runsite docs](https://docs.runsite.app).

## The short version

* There are three lines, not one. Operations decides whose calendar the work lands on, security decides who is accountable when a control fails, and data protection law decides who answers for personal data. They do not fall in the same place.
* The layer diagram is a map of what exists, not of what it costs. Two layers can be twenty minutes a year between them and one layer can be the reason someone was awake at 03:00.
* Read the fourth column, not the first. Almost nothing on the IaaS list is scheduled: certificates fail silently ninety days early, disks fill fastest on your best traffic day, and advisories are published when the researcher is ready.
* AWS's model splits on a preposition. Security of the Cloud is theirs, Security in the Cloud is yours, and a provider can be entirely correct about their half while your storage bucket is world-readable.
* Shared Controls is the row to watch. AWS defines it as controls applying to both layers "in completely separate contexts or perspectives". If nobody has written down who does what in your setup, that control is not shared, it is unassigned.
* PaaS shortens your job list and lengthens your boundary list. Fewer things to do, more edges where an assumption can hide.
* Your GDPR role does not change with the service model. You are the controller either way. What changes is how much of the stack has a processor attached and what that processor committed to in writing.
* On a virtual machine, the layers you took on have no processor behind them. That is not non-compliance, it is a line item, and it belongs in the comparison rather than a footnote.
* "Is Azure an IaaS or PaaS?" has no answer because the question classifies a company. The models classify services, and one vendor sells all three.
* The raw server wins when the boundary must sit below the OS, when the workload needs something a platform will not run, when your team already operates a fleet, or when you would rather own more and argue less.
* Every example in the current search results is American. On the IaaS side Europe has Hetzner, Scaleway, OVHcloud, Exoscale and UpCloud; on the platform side, check whether the region you were offered belongs to an operator under EU law.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is Azure an IaaS or PaaS?

Both, and so is every other large cloud vendor, because the service models describe individual services rather than companies. Azure Virtual Machines is IaaS: you get a machine and you manage the operating system on it. Azure App Service is PaaS: you supply an application and Microsoft operates the runtime beneath it. Microsoft 365 is SaaS. The same is true of AWS, where EC2 is IaaS and Elastic Beanstalk is PaaS, and of Google Cloud, where Compute Engine and App Engine sit on opposite sides of the same line. If you are trying to work out who is responsible for a given layer, the useful question is which model a specific service is sold under, and the answer is in that service's own documentation and terms rather than in the vendor's name.

### Is Netflix IaaS, PaaS, or SaaS?

It depends which direction you are looking from, which is why the question keeps getting asked. Viewed from below, Netflix is a customer of infrastructure services and therefore an IaaS tenant. Viewed from above, Netflix sells a finished application that subscribers use without touching any infrastructure, which makes it SaaS to its own users. Both descriptions are correct and they describe two different relationships. The general rule is that the service model labels a relationship between a provider and a customer, not a company, so a business can occupy a different position in each relationship it is party to.

### Which responsibility is in the shared responsibility model?

Every provider's version splits into at least three parts. The provider's part covers what AWS calls Security of the Cloud: physical data centre security, the hardware, the network, the hypervisor and the managed services themselves. Your part covers Security in the Cloud: your data and how it is classified and encrypted, identity and access management, the configuration of everything you were given knobs for, and your own application code. Between them sits a category AWS labels Shared Controls, which it defines as controls that apply to both the infrastructure layer and customer layers but in completely separate contexts. Patch management is the usual example: the provider patches the infrastructure, and you patch the guest operating system and application. Where the boundary lands depends on the service model, since IaaS leaves the operating system and everything above it with you while PaaS moves the operating system and runtime to the provider.

### What is an example of shared responsibility?

Patch management is the clearest one. On an IaaS virtual machine, the provider patches the hardware and hypervisor and you patch the kernel, the operating system packages, the language runtime and your dependencies, which in practice means either subscribing to advisories or enabling unattended upgrades and accepting reboots you did not schedule. On a platform, the provider rebuilds and patches the base image and runtime, and you remain responsible for your application dependencies, so a vulnerable library in your lockfile is still your problem on both sides. Configuration is a second example that catches more people out: the provider gives you a firewall, and whether port 5432 is exposed to the internet is a decision you made. In both cases the control genuinely applies to both parties, which is why writing down who does which half for your specific setup is worth the ten minutes it takes.

Keep reading

---

# Redis OOM command not allowed: What It Means

> Redis refuses writes but still serves reads. What maxmemory and noeviction do, which eviction policy you want, and when the fix is more memory.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. OOM command not allowed: Redis maxmemory and Eviction Policies

CachingSeptember 2, 202612 min read

# OOM command not allowed: Redis maxmemory and Eviction Policies

Redis is refusing writes and still serving reads. The error is not a crash, it is a policy doing exactly what you configured, and two defaults nobody chose put you there.

## The short version

It is not a crash. It is an eviction policy doing what you configured, and two defaults nobody chose put you there. The fix depends on one question: if everything in the instance can be recomputed, it is a cache — set allkeys-lru or allkeys-lfu and the error becomes cache misses. If not, it needs more memory.

[Teo Marquardt](https://runsite.app/about#author)

The application is up. Redis is up. Reads are fine. Every write returns the same line, and it has been returning it since about the time traffic picked up.

The first organic result for this error in Germany, as of writing, is a personal blog post from May 2016\. Below it is a Stack Overflow question that is thirteen years old, then a YouTube video, then a GitHub issue from 2018 and a thread on the redis-db mailing list. It is a well-documented error with a decade of answers behind it, and most of those answers stop at the same two suggestions: give it more memory, or turn on eviction.

Both can be right. Which one is right depends on a question none of those pages ask first, and getting it backwards either loses data or papers over a leak. This walks the error apart, explains the two defaults that produce it, and gets to the question that actually decides the fix.

## OOM command not allowed when used memory > 'maxmemory'.

That is the whole string, single quote and full stop included. Four facts are packed into it and three of them are commonly misread.

* **It is not a crash.** Redis is running and answering. `GET`, `EXISTS`, `TTL` and `DEL` all still work. What has stopped is the class of commands that would make the dataset bigger, which is why the symptom reaches you as writes failing rather than as an outage.
* **It is not the Linux OOM killer.** The names collide and the situations do not. The kernel's out-of-memory killer terminates a process and leaves you with a dead server and a line in `dmesg`. This is Redis declining a command on purpose, and the process is fine.
* **`maxmemory` is a limit you set**, not a property of the machine. The error is Redis reporting that it reached a ceiling someone configured, which is either you, your configuration management, or the defaults of whatever image you deployed.
* **The quotes are part of the message.** `'maxmemory'` appears in single quotes in the error text itself, which matters only when you are grepping logs for it and wondering why an exact-match search returns nothing.

So the server is behaving correctly. It hit a limit and applied a policy. The interesting part is that in a default installation, nobody picked either of them deliberately.

## The two defaults that produce this error

Redis ships with two settings whose defaults are individually reasonable and jointly surprising. As of writing, on a 64-bit build ([Memcached reaches the same ceiling differently](https://runsite.app/blog/redis-vs-memcached), with a limit that is mandatory and a policy you do not get to pick):

ini

```
# redis.conf — the two lines this error is about

# No limit. Redis will grow until something else stops it.
maxmemory 0

# When a limit exists, refuse writes rather than delete anything.
maxmemory-policy noeviction
```

Read them together and the sequence is clear. Out of the box there is no ceiling, so this error cannot happen; instead Redis grows until the kernel kills it, which is the failure people meet first. Then someone sets `maxmemory` to stop that, which is correct. The ceiling now exists, `noeviction` is still in place because nobody touched it, and the failure mode changes from a dead process to refused writes.

Neither default is wrong. `noeviction` is the safe choice for a Redis that holds something you cannot lose, and defaulting to silent data deletion would be worse. The problem is that the second setting is usually inherited rather than chosen, and it is the one that decides what your outage looks like.

### Why `maxmemory 0` is not the same as "no limit"

There is always a limit. Setting `maxmemory 0` moves it from Redis to the operating system, where it is enforced by the kernel with no notice, no policy and no clean handling. A container with a memory limit will be killed at that limit, and the container [exits with code 137](https://runsite.app/blog/exit-code-137); a virtual machine will start swapping first, which for an in-memory store is worse than being killed.

The Redis FAQ suggests a conservative rule for where to put the ceiling: "keep 20% of free memory available in the system beyond the configured maxmemory". The headroom is not padding. Replication buffers, the client output buffers and the copy-on-write pages produced during a background save all live outside the accounting that `maxmemory` governs, and they arrive exactly when the instance is busiest.

## Eight policies, two axes

The documentation lists eight values for `maxmemory-policy` and most articles reproduce the list. It is easier to hold as a grid: first decide which keys are eligible for eviction, then decide how Redis picks among them.

| Eligible keys                   | Least recently used | Least frequently used | Random          | Shortest TTL |
| ------------------------------- | ------------------- | --------------------- | --------------- | ------------ |
| Every key                       | allkeys-lru         | allkeys-lfu           | allkeys-random  | —            |
| Only keys with a TTL set        | volatile-lru        | volatile-lfu          | volatile-random | volatile-ttl |
| None — refuse the write instead | noeviction          | noeviction            | noeviction      | noeviction   |

The eight values as a grid rather than a list. LFU variants were added in Redis 4.0; check the policies your version actually accepts before putting one in a config file.

Two details about the selection half are worth knowing before you rely on it. The LRU is approximate: rather than maintain a true ordering, Redis samples a handful of keys and evicts the best candidate from the sample, with the sample size controlled by `maxmemory-samples`. It is close enough for a cache and it is not a guarantee about any individual key. LFU counts accesses rather than recency, which is the better choice when a small set of keys is hot and a long tail is touched once and never again. Which of the two fits depends on how your code fills the cache, and [the write pattern you chose](https://runsite.app/blog/cache-aside-write-through-write-behind) largely decides that.

### Redis maxmemory-policy noeviction but keys disappear without error

That is a Stack Overflow title, quoted as written, and it names the trap in the `volatile-*` half of the grid from the other direction.

A `volatile-*` policy can only evict keys that have a TTL. Plenty of cache code sets keys without any expiry, because the cache is expected to be replaced rather than to expire, and in that case the eligible set is empty. Redis has a policy, the policy has nothing to work with, and the behaviour collapses back to refusing writes. You get the `noeviction` outcome while the configuration says something else, which is the version of this problem that takes longest to diagnose.

One of the threads Google surfaces for this error is a user on the redis-db list reporting exactly the opposite confusion: "maxmemory is set to 500M with maxmemory-policy "volatile-lru", I'm setting TTL for each key sent". Both halves of the mistake are common. The rule to remember is that `volatile-*` is a promise about a subset, and you have to be the one who guarantees the subset is not empty.

## The question that decides everything: cache or store?

Here is the fork that the top results skip. Before choosing a policy, answer one thing: if Redis silently dropped a random key right now, would anything be lost that does not exist somewhere else?

If the answer is no, Redis is a cache. Every key can be recomputed from the database or the origin, eviction is a cache miss and a cache miss is a slow request rather than an incident. `allkeys-lru` or `allkeys-lfu` is almost certainly what you want, and leaving `noeviction` in place on a pure cache means you have configured an outage to protect data that was never at risk.

If the answer is yes, Redis is a store, and this changes the whole shape of the fix. Sessions that exist nowhere else, a job queue, a rate-limiter's counters, a lock, the only copy of a computed result: evicting any of those is data loss that arrives without an error, because eviction is not an error. On a store, `noeviction` is the correct setting and the error you are reading is the system working. The fix is more memory, less data, or moving some of it out. Never a policy change. If sessions are the durable part, [where sessions should live in the first place](https://runsite.app/blog/redis-vs-database-for-sessions) is worth deciding before memory runs out.

The mixed instance is the real problem

Most instances that hit this are both at once: a cache and a queue and a set of sessions in one keyspace, because they started as a cache and grew. Rate-limit counters are the classic stowaway, and [an evicted counter means the limit silently stops applying](https://runsite.app/blog/rate-limiting-429-retry-after). There is no policy that is right for a mixed instance, which is the actual finding. Separate the durable data onto its own instance or database and the policy question answers itself on each side. [What belongs in a cache in the first place](https://runsite.app/blog/when-to-use-redis-cache) is the version of this decision made early, when it is cheap.

## Diagnosing it before it happens

Two `INFO` sections tell you everything relevant, and the second one is the one worth alerting on.

bash

```
redis-cli INFO memory | grep -E 'used_memory_human|maxmemory_human|maxmemory_policy|mem_fragmentation_ratio'

# used_memory_human:3.71G
# maxmemory_human:4.00G
# maxmemory_policy:noeviction
# mem_fragmentation_ratio:1.19

redis-cli INFO stats | grep -E 'evicted_keys|keyspace_misses'

# evicted_keys:0
# keyspace_misses:184122
```

`evicted_keys` is the number that separates the two situations above without needing an opinion. On a cache with an `allkeys-*` policy it should be non-zero and rising steadily, which means the policy is doing its job; a sudden climb means your working set outgrew the instance. On an instance where it sits at zero while memory approaches the ceiling, you are on `noeviction` and the write refusals are queued up behind that number.

The useful alert is on the ratio of `used_memory` to `maxmemory` rather than on the error itself, because by the time the error appears users have already seen it. `mem_fragmentation_ratio` is worth a glance in the same breath: a value well above 1 means the allocator is holding memory the dataset is not using, and the ceiling arrives sooner than the key count suggests.

## Eviction is not deletion, and your DPO will ask

Caches hold personal data. Sessions hold user identifiers, cached API responses hold whatever the API returned, and a rate limiter is a record of who did what and when. That makes eviction policy a question with a second audience.

An eviction policy is not a retention policy and it is not a mechanism for erasure. `volatile-ttl` looks like retention because it involves expiry times, but it removes whatever is closest to expiring when memory runs short, so the order is driven by pressure rather than by obligation. If a user asks for their data to be erased, an eviction policy has no opinion on the matter and may hold their session for another week while dropping someone else's.

Two practical consequences. Whatever you would say to an auditor about how long cached personal data lives has to come from explicit TTLs and explicit deletion on the erasure path, not from the eviction policy. And the instance holding that data is in a jurisdiction: [where the data physically rests and who is a processor for it](https://runsite.app/blog/gdpr-data-residency) is a separate question from how it is evicted, and the answer to the first one is not improved by the second.

## Not this error: "oom prevention" in other tools

If you arrived here from a video editor

One of the searches Google associates with this error is "Oom command not allowed under oom prevention opus clip", which is a different product's message entirely — Opus Clip is a video tool with its own out-of-memory guard. Nothing on this page applies to it. The Redis error always contains the words `when used memory > 'maxmemory'`; if yours does not, you are looking at a different system that borrowed the same three letters.

## How Runsite handles it

On [managed Redis hosted in the EU](https://runsite.app/services/redis), the memory ceiling comes from the plan rather than from a config file nobody edited, so the `maxmemory 0` half of this problem does not arise: there is a defined limit, it is visible, and the instance is monitored against it rather than against the machine. What the ceiling does not decide for you is the policy, because that depends on what you put in there, and the cache-or-store question above is one only your application can answer.

Servers are in Germany as of writing, and a signed GDPR data processing agreement covers every account, which matters here specifically because sessions and cached responses are the personal data the previous section is about. If the honest answer to the cache-or-store question turns out to be "store", [managed PostgreSQL in the EU](https://runsite.app/services/postgresql) is the cheaper place to keep the part that must survive, and Redis goes back to being a cache.

Being fair about it: nothing here is unique to a managed service. A `maxmemory` value, a policy chosen on purpose, an alert on the memory ratio and a look at `evicted_keys` get you the same result on your own server. The difference is whose calendar the first three land on. Setup details are in the [Runsite docs](https://docs.runsite.app).

## The short version

* `OOM command not allowed when used memory > 'maxmemory'.` is not a crash and not the Linux OOM killer. Redis is running, reads are being served, and commands that would grow the dataset are being declined on purpose.
* Two defaults produce it. `maxmemory 0` means no Redis-level limit, so the kernel enforces one instead; once you set a real ceiling, the untouched `maxmemory-policy noeviction` turns the failure from a dead process into refused writes.
* There is always a limit. `maxmemory 0` only decides whether it is enforced by Redis with a policy or by the kernel without one.
* Leave headroom. The Redis FAQ suggests keeping 20% of system memory free beyond the configured `maxmemory`, because replication buffers, client output buffers and background-save copy-on-write pages sit outside that accounting.
* The eight policies are a grid, not a list: which keys are eligible (`allkeys-*`, `volatile-*`, or none) crossed with how one is picked (`lru`, `lfu`, `random`, `ttl`).
* A `volatile-*` policy with no TTLs anywhere behaves exactly like `noeviction`, and that is the version of this that takes longest to find.
* The LRU is approximate. Redis samples candidates rather than maintaining a true ordering, which is right for a cache and is not a promise about any one key.
* Answer cache-or-store before touching the policy. On a pure cache, `noeviction` is an outage protecting data that was never at risk. On a store, `noeviction` is correct and the fix is memory, not a policy change.
* A mixed instance has no correct policy. Split the durable data out and each side answers for itself.
* Alert on the ratio of `used_memory` to `maxmemory`, and watch `evicted_keys`: zero while memory climbs means you are on `noeviction` and the refusals are coming.
* Eviction is not erasure. It removes what is convenient, not what you are obliged to delete, so retention has to come from explicit TTLs and an explicit deletion path.

Related service

## Managed Redis on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed Redis](https://runsite.app/services/redis)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### How to fix OOM error?

In Redis, start by deciding what the instance is for, because the two fixes are opposites. If everything in it can be recomputed from another source, it is a cache: set \`maxmemory-policy\` to \`allkeys-lru\` or \`allkeys-lfu\` and Redis will make room by evicting the least useful keys, turning the error into cache misses. If any of it exists nowhere else — sessions, a job queue, rate-limiter counters, a lock — then evicting is data loss, \`noeviction\` is the correct setting, and the fix is to raise \`maxmemory\`, reduce what you store, or move the durable part elsewhere. Two things to check before either: whether \`maxmemory\` is set at all, since a default of 0 means the kernel enforces the limit instead, and whether a \`volatile-\*\` policy is configured against keys that have no TTL, which makes it behave like \`noeviction\` while appearing to be something else.

### How can I fix Redis using too much memory?

Measure before changing anything. \`INFO memory\` gives you \`used\_memory\_human\` against \`maxmemory\_human\` and the policy in force; \`INFO stats\` gives you \`evicted\_keys\`, which tells you whether eviction is happening at all. \`mem\_fragmentation\_ratio\` well above 1 means the allocator is holding memory the dataset is not using, so the real dataset may be smaller than the number suggests. From there the usual causes are keys written without a TTL that were expected to be temporary, values that grew larger than intended, and a working set that has genuinely outgrown the instance. Setting an explicit \`maxmemory\` with an eviction policy that matches what the data is for converts an unbounded problem into a bounded one, which is the point rather than a workaround. Leave headroom beyond the ceiling: the Redis FAQ suggests keeping about 20% of system memory free, because replication and client output buffers are not counted inside it.

### What is maxmemory?

\`maxmemory\` is a Redis configuration directive that sets the maximum amount of memory the dataset may occupy, in bytes or with a suffix such as \`2gb\`. Its default on 64-bit builds is 0, which means no Redis-level limit at all. When the limit is reached, Redis consults \`maxmemory-policy\` to decide what to do: evict keys to make room, or refuse commands that would increase memory use and return the OOM error. The accounting covers the dataset rather than the whole process, so replication buffers, client output buffers and the copy-on-write pages created during a background save can push actual usage above the configured figure. That is why the recommended practice is to set the ceiling below the memory available to the machine or container rather than equal to it.

### What is the maximum memory a Redis instance can use?

There is no fixed ceiling in the software; on a 64-bit build the practical limit is whatever memory the machine or container makes available, and what you configure with \`maxmemory\` is a policy decision rather than a hardware one. The useful maximum is lower than the total for two reasons. Memory outside the dataset accounting needs room: replication buffers, client output buffers, and pages copied during a background save all sit there, which is where the Redis FAQ's suggestion of keeping 20% of system memory free comes from. And an in-memory store that starts swapping performs far worse than one that evicts, so the ceiling should be reached before the operating system runs short rather than after. If a single instance genuinely needs more than a large machine can hold, the answer is usually to split the keyspace across instances by purpose, which also resolves the mixed cache-and-store problem that produces most of these errors.

Keep reading

---

# Cron Jobs vs Background Workers vs Queues

> Three primitives, three questions, three ways to fail. When a schedule is enough, when you need a worker or a queue, and where the job actually runs.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Cron Jobs, Background Workers and Queues: Choosing the Right One

SchedulingSeptember 4, 202614 min read

# Cron Jobs, Background Workers and Queues: Choosing the Right One

Three primitives that answer three different questions. What each one does when it fails, why the syntax half of this topic is disappearing from search, and where the job actually runs.

## The short version

Three primitives answering three different questions. Cron is for work that has a time; a worker is for work that has a trigger; a queue is for work that must survive being handed off. What separates them in practice is what each does when it fails, and a missed cron run is the failure that leaves no trace.

[Teo Marquardt](https://runsite.app/about#author)

The page Google still ranks first for this comparison is a thread titled "Do I need a worker, cron job or both?" It was posted to r/webdev eleven years ago and it has three comments.

Eleven years and three comments is not a knock on the thread. The top answer is good: workers are for things that happen in response to an event, crons are for things that happen at a time. It is a comment on the question, which has been asked continuously since 2015 and has never been given a page that settles it. Below the thread sit a small blog, Railway's documentation, a Stack Overflow question, an article from Render, a Nextcloud support thread and a LinkedIn post.

This is the pillar for how recurring and deferred work gets run. It covers the three primitives, the questions each one answers, what each does when it goes wrong, and one thing none of the pages above raise: where the job physically executes and who is responsible for the data it touches while it runs.

One word of warning about searching for this

"Background jobs" is a homonym, and in Google's German results it is mostly not about software: the query returns an employment widget, and the surrounding searches are for work as a film extra (`background actor jobs`, `background dancer jobs`) and for pre-employment background checks. The rest is SAP administration. If your searches are coming back strange, that is why. Pair the phrase with `worker`, `queue` or `cron` and the results change completely.

## Three primitives, three different questions

Render puts the distinction well enough to quote rather than paraphrase: "Cron jobs answer when work runs. Background workers answer how much work you can absorb." That is two of the three. The queue is the third, and it answers what happens to work you cannot absorb yet.

| Primitive         | The question it answers                          | What starts it                 | When it ends                                                  |
| ----------------- | ------------------------------------------------ | ------------------------------ | ------------------------------------------------------------- |
| Cron job          | \*When\* does this run?                          | A clock                        | When the command exits. It is expected to exit.               |
| Background worker | \*How much\* concurrent work can we absorb?      | Process start, once, at deploy | Never, by design. Restarting it is an event, not a routine.   |
| Queue             | \*What happens\* to work we cannot do right now? | Your application, on an event  | Not a process at all. It is the buffer the worker reads from. |

A queue is not a third kind of process. It is state that sits between whatever produces work and whatever consumes it, which is why most real systems use a worker and a queue together rather than choosing between them.

Railway's documentation makes the worker half concrete: "A background worker is an always-on service that processes work continuously. Unlike a cron job, it does not exit after completing a task." The word doing the work in that sentence is \*exit\*. A cron job that does not exit is a bug, because the next scheduled run is coming. A worker that exits is an outage, because nothing is going to start it again until something notices.

That gives you a usable first cut. Nightly invoice generation is a cron job: it has a time, it finishes, and nobody is waiting. Resizing an uploaded image is a queue and a worker: it has no natural time, someone is waiting, and the arrival rate is set by your users rather than by you. So is a GitHub webhook receiver, where [the sender hangs up after 10 seconds](https://runsite.app/blog/github-webhook-timed-out) and the only safe move is to store the payload and answer. Sending a password reset email is neither, most of the time; it is a request that should just be handled, and pushing it into a queue buys reliability at the cost of latency the user can feel.

## What each one does when it fails

The comparison usually stops at the table above. The table is the easy half, and it is not where the decision actually goes wrong. Each primitive has a characteristic failure, and picking the wrong one means signing up for a failure mode you have not thought about.

|                            | Cron job                                           | Worker                                            | Queue                                                |
| -------------------------- | -------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------- |
| What breaks first          | The run that never happened                        | The process that quietly died                     | The backlog nobody is draining                       |
| How you find out           | Something downstream is stale, usually days later  | Latency climbs, then work stops arriving anywhere | Depth metric, if you have one. Otherwise a customer. |
| Does it retry?             | No. Classic cron does not catch up on missed runs. | Only if you wrote the retry                       | Usually yes, and that is its main advantage          |
| Failure you did not expect | Two runs overlapping when one is slow              | One instance doing work twice after a restart     | A poison message retried forever                     |

The failure column is the part worth choosing on. Every option here can be handled; none of them is handled by default.

### The run that never happened

A cron job that does not fire produces no error, no log line and no alert. There is nothing to fail. If the machine was asleep, restarting, or in the middle of a deploy at 03:00, the 03:00 run does not happen and classic cron does not go back for it. This is the reason `anacron` exists on desktop and laptop systems, and it is worth knowing the distinction as of writing: `anacron` works in periods rather than at times and will run a job late if it was missed, while `cron` will not.

Deploys are the case that catches teams out, because a deploy is exactly when the machine is least likely to be able to run anything, and deploys cluster in working hours while nightly jobs do not. [Rolling releases behind a health check](https://runsite.app/blog/git-push-to-deploy) narrow the window without closing it. The practical defence is that the job should notice: a task that records when it last completed, and an alert on that timestamp being older than it should be, catches the silent case that nothing else will.

### Background Job Workers instead of Cron?

That is a Nextcloud support thread from January 2026, quoted as titled. The user writes: "I've been using systemd timer, and crontab before that, but after updating to the latest version a week ago background jobs weren't running." It is the whole category in one sentence. The schedule was fine. The thing the schedule was supposed to start had stopped working, and the schedule had no opinion about that.

The second failure in this family is overlap. A job scheduled every five minutes that starts taking six will eventually be running twice, and if it was not written for that, the two copies will fight over the same rows. What should happen next is a real decision with three sane answers: skip the new run, queue it, or kill the old one. That decision has its own name and its own article coming, so it is enough here to say that if a scheduled job can ever run long, you have already made the decision by default and the default is "run both".

`systemd timer` is worth naming as the honest alternative on a single machine. It handles missed runs, it logs to the journal, and it can express dependencies that a crontab line cannot. It is also one more thing to administer on a box you maintain, which is the trade the next section is about.

## Is cron outdated?

Google asks this question itself. "Is cron outdated?" appears in the People Also Ask block on both the comparison search and the plain `cron job` search, sitting next to "What replaced cron jobs?". Two versions of the same suspicion, surfaced by Google rather than by a vendor.

The measurable answer is more interesting than yes or no. Search demand for this vocabulary in Germany is falling across the board year on year as of writing, and the steepest falls are all in one half of it: `python cron job` is down 71%, `cron job raspberry pi` 64%, `cron job every 5 minutes` 56%, `cron job logs` 56%, `cron job ubuntu` 46%, `cron job expression` 32%, `cron job syntax` 18%. Not one phrase in the set is growing.

Look at what is falling and the pattern is not that people stopped scheduling things. It is that they stopped searching for how to write the schedule. The reason is visible in the results themselves: Google's generated answer for `cron job` now prints the five-field table and this line before any link at all.

bash

```
* * * * * /path/to/script.sh
# minute  hour  day-of-month  month  day-of-week
```

So the honest answer to "is cron outdated" is that the primitive is fine and the craft around it has been absorbed. Scheduling a job at a time remains the correct shape for a large class of work, and no queue replaces it. What has aged is hand-editing a crontab on a machine you personally administer, along with the assumption that knowing the syntax is the skilled part of the job. The syntax is now the part a search engine hands you for free; what it does not hand you is the failure table above.

### What replaced cron jobs?

Nothing replaced the primitive. What changed is where it runs. On a single server the successor to a crontab line is a `systemd timer`, and in a cluster it is a Kubernetes `CronJob`, which as of writing does the same thing with a controller behind it. On a managed platform it is a scheduled job defined next to the application, which is the version that removes the machine from the picture entirely.

Google is confused about the words too

One of the four questions in that People Also Ask block reads, exactly as written: "What is the difference between a cron job and a cron job?" It is not a typo on this page. The term genuinely carries three meanings that people use interchangeably — the daemon, the crontab entry, and the scheduled unit of work a platform runs for you — and the question, broken as it is, is a fair summary of the state of the vocabulary.

## Where the job actually runs, and who is the processor for it

None of the pages ranking for this comparison ask where the work executes. It is a strange omission, because a background job is usually the part of a system that touches the most data at once.

Consider what a routine nightly job does. It connects to the production database and reads rows about real people. It writes a log line for each one. It renders and sends email, so it hands names and addresses to a third party. It may write a report to object storage that nobody has classified. A request handler touches one user's data; a batch job touches everyone's, and it does so unattended, at an hour when nobody is watching.

That makes the execution location a question with legal weight rather than an operational detail. Under the GDPR you remain the controller for all of it, and whoever runs the machine the job executes on is a processor for that layer. [Which layers come with a processor attached and which you carry alone](https://runsite.app/blog/paas-vs-iaas-responsibility) depends on the service model you bought, and it is the same line for a worker as for the web application. The email relay, the error tracker and the storage bucket the job wrote to are sub-processors you inherited, and [what your data processing agreement has to cover](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) includes all of them.

Two practical points fall out of that. Jobs and application should run in the same jurisdiction, because a scheduler in one region reaching into a database in another is a transfer whether or not anyone described it as one. And job logs deserve the same retention thinking as any other store of personal data, which they rarely get, because a log line written at 03:00 by a script nobody reads is the easiest place in a system to accumulate records indefinitely.

## Choosing: four questions, not a feature table

* **Does the work have a time, or a trigger?** A time means a schedule. A trigger means a queue and something reading it. If the answer is "a trigger, but we check for it on a schedule", you have a queue implemented as a cron job polling a table, which works and is worth naming so you know what you built.
* **Is anyone waiting for it?** Nobody waiting means a schedule is fine and running late costs nothing. Someone waiting means latency is a feature, and the arrival rate is set by your users rather than by you, which is the definition of the case a worker exists for.
* **What happens if it runs twice?** If the answer is "nothing", most of the difficulty in this article disappears. If the answer is "we double-charge someone", then idempotency is the actual requirement and the choice of primitive is secondary to it. "Twice" is also more likely than it sounds: [a schedule defined inside an application running on several instances fires once per instance](https://runsite.app/blog/cron-job-multiple-instances).
* **Does it call an API with a quota?** If the work depends on an upstream that can refuse you, the queue stops being optional. [Handling a 429 and the Retry-After header](https://runsite.app/blog/rate-limiting-429-retry-after) is a retry problem, and a retry problem wants somewhere durable to retry from.
* **What happens if it never runs?** If the answer is "we find out from a customer", the missing piece is not a primitive at all. It is a completion timestamp and an alert on it, and that applies equally to all three options.

Most production systems end up with all three, which is the outcome the r/webdev thread arrived at eleven years ago. A schedule for the work that has a time, a queue for the work that has a trigger, and a worker draining the queue. The reason to be deliberate about which is which is that they fail differently, and a system where all three are the same thing fails in whichever way you were least prepared for.

## How Runsite handles it

Both shapes exist on the platform rather than only one. [Scheduled jobs run as cron jobs on Runsite](https://runsite.app/services/cron-jobs) against the same repository and the same environment as your application, so the schedule is deployed with the code instead of living in a crontab on a machine somebody has to remember. The always-on shape is a [web service](https://runsite.app/services/web-services) that happens not to serve HTTP: a worker process started at deploy and restarted with the application.

The queue is the piece we do not invent. [Managed Redis](https://runsite.app/services/redis) is where most teams put one, and it is worth being clear that [using Redis as a queue rather than as a cache](https://runsite.app/blog/when-to-use-redis-cache) changes what happens when it fills up, because a queue is durable data and [an eviction policy will drop it without an error](https://runsite.app/blog/redis-oom-command-not-allowed). The job's data lives in [managed PostgreSQL](https://runsite.app/services/postgresql) and whatever it writes goes to [object storage](https://runsite.app/services/s3-storage) on the same invoice.

On the jurisdiction question from the previous section: servers are in Germany as of writing, so the scheduler, the worker, the database and the storage are in the same place, and a signed GDPR data processing agreement covers every account. That is the specific thing a crontab on your own box cannot give you, because there the obligation for the layer the job runs on has nowhere to sit except with you. A worked example of a real scheduled task, written out end to end, is [the automated PostgreSQL backup to S3](https://runsite.app/blog/postgresql-backup-to-s3). Setup details are in the [Runsite docs](https://docs.runsite.app).

## The short version

* Three primitives answer three questions. Cron answers when work runs, a worker answers how much concurrent work you can absorb, and a queue answers what happens to work you cannot absorb yet.
* A queue is not a third kind of process. It is the state between the thing producing work and the thing consuming it, which is why workers and queues are usually bought together.
* The word that separates cron from a worker is \*exit\*. A cron job that does not exit is a bug; a worker that exits is an outage.
* Choose on the failure mode, not the feature table. A missed cron run produces no error at all, a dead worker shows up as latency, and a queue nobody drains shows up as a customer complaint.
* Classic cron does not catch up. A run that fell inside a reboot or a deploy window is simply gone, which is what `anacron` was built to address and what a completion timestamp plus an alert actually solves.
* If a scheduled job can ever run long, you have already chosen an overlap policy and the default is "run both".
* "Is cron outdated?" is Google's own question, in the People Also Ask block on both searches. The primitive is not outdated; hand-editing a crontab on a machine you administer is.
* The demand data says the same thing. Every measurable phrase in this vocabulary is falling year on year in Germany as of writing, and the steepest falls are all syntax queries, because Google's generated answer now prints the five fields before any link.
* Nothing replaced cron. `systemd timer` on one machine, `CronJob` in a cluster, a scheduled job next to the application on a platform. The primitive moved, it did not retire.
* A batch job touches everyone's data at once, unattended. Where it executes is a question about jurisdiction and sub-processors, not a deployment detail, and no page ranking for this comparison raises it.
* Ask four questions: does the work have a time or a trigger, is anyone waiting, what happens if it runs twice, and what happens if it never runs. The last one is answered the same way whichever primitive you pick.

Related service

## Cron Jobs on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Cron Jobs](https://runsite.app/services/cron-jobs)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is cron outdated?

The primitive is not. Running a command at a fixed time remains the right shape for a large class of work, and no queue or event system replaces it: nightly reports, cleanup tasks, billing runs and backups all have a time rather than a trigger. What has aged is the practice around it. Hand-editing a crontab on a server you personally administer is now the unusual choice, replaced by \`systemd timer\` on a single machine, Kubernetes \`CronJob\` in a cluster, or a scheduled job defined alongside the application on a managed platform. Search behaviour reflects the shift rather than a decline in scheduling: in Germany as of writing, every measurable phrase in this vocabulary is falling year on year, and the steepest falls are syntax queries such as \`cron job every 5 minutes\` at 56% and \`python cron job\` at 71%, because a search engine now answers those inline. Demand for the architecture question has not gone anywhere.

### What replaced cron jobs?

Nothing replaced the concept; the place it runs changed. On a single Linux machine the modern equivalent of a crontab entry is a \`systemd timer\`, which logs to the journal, can express dependencies on other units and will run a job that was missed while the machine was off. In a Kubernetes cluster it is the \`CronJob\` resource, which creates a Job on a repeating schedule with a controller behind it. On a managed platform it is a scheduled job defined next to the application and deployed with it, which removes the machine from the question entirely. For work that is triggered by an event rather than by a clock, the replacement is a different primitive rather than a different scheduler: a queue that the application writes to and a background worker that drains it, which is a better fit whenever someone is waiting for the result or the arrival rate is set by your users.

### What is the difference between a cron job and a batch job?

A cron job is defined by when it starts and a batch job by how it processes. Cron is a scheduling mechanism: it runs a command at times you specify and has no opinion about what that command does. A batch job is a workload shape, meaning a large volume of records processed together in one pass rather than one at a time as they arrive, and it is defined by the processing style rather than by what triggered it. The two overlap constantly, since most batch jobs are started by a schedule, which is why the terms get used interchangeably. They come apart at the edges: a cron job that pings a health endpoint every minute is not batch work, and a batch job triggered by a file landing in storage is not scheduled. The distinction matters when the batch grows, because a batch job that outgrows its window will still be running when the schedule fires again, and overlapping runs are a different problem from a slow one.

### What exactly is a cron job?

A cron job is a command registered to run automatically at specified times, executed by \`cron\`, a time-based scheduler that has been standard on Unix-like systems for decades. The schedule is written as five fields covering minute, hour, day of month, month and day of week, followed by the command, and the entries live in a table called a crontab. In practice the term now covers three related things that people use interchangeably: the daemon itself, an individual crontab entry, and the scheduled unit of work that a cloud platform runs on your behalf without any crontab involved. The last of those is what most developers mean today. The properties worth remembering are that a cron job is expected to exit when it finishes, that classic cron does not catch up on runs missed while the machine was unavailable, and that nothing prevents a second run from starting while the first is still going.

Keep reading

---

# 429 Too Many Requests: Both Sides of Retry-After

> 429 is a control signal, not a failure. How to read Retry-After when you are limited, what to send when you are limiting, and where Redis belongs.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. 429 Too Many Requests: Rate Limiting from Both Sides

CachingAugust 28, 202613 min read

# 429 Too Many Requests: Rate Limiting from Both Sides

You are usually on both ends of this at once — limited by somebody's API and limiting your own. One header joins the two sides, and almost nobody sends or reads it properly.

## The short version

429 is not an error, it is an instruction. When you are being limited, read Retry-After before guessing — and when it is missing, back off exponentially with jitter, which is the half that actually matters. When you are the one limiting, remember the counters are durable data living in a keyspace you probably treat as a cache.

[Teo Marquardt](https://runsite.app/about#author)

Search for this status code and Google will tell you to clear your browser cache, disable your extensions, deactivate WordPress plugins one at a time, and switch networks or use a VPN to change your IP address. That is genuinely the advice in the generated answer at the top of the results as of writing, and for someone who cannot load a shopping site it is the right advice.

It is not the advice you need if you write the software. In the eleven items on that first page, exactly one is aimed at a developer: a Stack Overflow question from twelve years ago, from which Google highlights four words that are the best summary of the whole topic.

This is written for the developer end of it, and specifically for the situation almost everyone is actually in — being rate limited by an API you depend on while rate limiting the API you operate. They are the same problem seen from two ends, and one HTTP header joins them.

## 429 is not an error

That is the phrase Google pulls out of that Stack Overflow answer, and it is worth taking literally. The answer continues: "1) Sleep your process. 2) Exponential backoff. If the server does not tell you how long to wait, you can retry...".

A 429 is the server successfully telling you something. Nothing failed: your request was understood, it was well-formed, you were authenticated, and the answer is that you are going faster than the agreement allows. Compare it with a 500, which means something broke and retrying might work, or a 403, which means stop and do not come back. A 429 means come back later, and it usually says how much later. Its neighbour 413 is the opposite case: the body is too big, and [retrying a 413 Request Entity Too Large](https://runsite.app/blog/413-request-entity-too-large) only gets the same refusal.

The distinction matters because of what a client does next. Code that treats 429 as a failure typically does one of two things, and both make it worse. It retries immediately, adding load to a server that just asked for less. Or it raises an exception into a queue of pending work, so the retry storm arrives all at once when someone restarts the process. Treating it as a control signal instead means reading what the signal says.

## Retry-After, and what to do when it is missing

The header exists for exactly this. A well-behaved limiter answers like this:

http

```
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30

{"error":"rate_limit_exceeded","detail":"100 requests per minute per API key"}
```

`Retry-After` takes two forms and clients need to handle both. It is either a number of seconds to wait, as above, or an HTTP-date giving the moment the limit lifts. The date form is the one that breaks naive parsers, and it is the one a limiter that resets on a fixed clock boundary will usually send.

The `RateLimit-*` family is the newer half of that response and the useful part for a well-written client, because it can slow down before hitting the wall rather than after. These headers are the subject of an IETF draft as of writing, so the exact names are worth checking against your dependencies; the `X-RateLimit-*` spellings remain widespread as the de facto predecessor and plenty of APIs still send those instead.

### Exponential backoff with jitter, and why the jitter is the important half

When there is no `Retry-After`, you back off. Doubling the wait after each failure is the well-known part and it is not sufficient on its own, because every client that started at the same time doubles on the same schedule and they all come back together. The randomness is what breaks up the convoy.

python

```
# Wait for what the server asked for; back off with jitter when it did not say.
delay = response.headers.get("Retry-After")

if delay is not None:
    wait_seconds = parse_retry_after(delay)   # seconds, or an HTTP-date
else:
    base = min(MAX_BACKOFF, INITIAL_BACKOFF * 2 ** attempt)
    wait_seconds = random.uniform(0, base)    # full jitter, not base + jitter
```

Two details that get skipped. A cap on the backoff keeps a long outage from producing a retry scheduled an hour out, which is usually worse than failing cleanly. And a limit on attempts matters more than the delay curve: work that will never succeed should end up somewhere a human can see rather than being retried until the process restarts.

## When you are the one being limited

Look at what people actually search alongside this status code and it is a directory of other people's products: Shopify, HubSpot, GitLab, Steam, Docker, Spotify, DeepL, Okta, Etsy, npm, Jira, Discord, Deutsche Bahn. The 429 you are reading is nearly always somebody else's decision about you, delivered after the fact.

That shapes the response. You cannot negotiate with it in code, so the work is to (a) find the documented limit before you meet it, (b) make the limit visible in your own monitoring rather than discovering it in a support ticket, and (c) decide which calls are allowed to fail and which have to be retried later, which is a queue question rather than a client question.

### 429 Too Many Requests OpenAI

That is a real related search on this results page, sitting next to `429 too many requests chatgpt` and `429 too many requests gemini`. Model APIs are now one of the most common sources of this status code, and they behave differently enough from a normal REST quota to be worth separating.

As of writing, these limits are typically two-dimensional: a cap on requests per minute and a separate cap on tokens per minute, and you can be nowhere near the first while sitting on the second. A long prompt consumes quota that a short one does not, so the same number of calls can pass in the morning and fail in the afternoon because the inputs got bigger. Retrying a second later achieves nothing when the window is a minute wide and your own traffic is what filled it.

The structural answer is that model calls belong behind a queue rather than in a request handler. The work is slow, the quota is shared, and the failure is temporary, which is the exact profile the [worker and queue shape](https://runsite.app/blog/cron-jobs-workers-queues) exists for. A user waiting on a synchronous call gets an error; a job in a queue gets retried in a minute and nobody notices.

## When you are the one limiting

The other end. Three algorithms cover almost everything, and they differ mainly in what they do at the boundary of the window.

| Algorithm           | What it stores                            | Behaviour at the boundary                                                                         | When to pick it                                                           |
| ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Fixed window        | One counter per key per window            | Allows a double burst: the full quota at the end of one window and again at the start of the next | Cheap, simple, fine when the limit is a courtesy rather than a protection |
| Sliding window      | A counter per sub-window, or timestamps   | Smooth, no double burst, at the cost of more memory and more work per request                     | Public APIs where the boundary burst is the abuse case                    |
| Token bucket / GCRA | A token count and a last-refill timestamp | Allows a deliberate burst up to bucket size, then enforces a steady rate                          | Interactive traffic, where a short burst is normal user behaviour         |

Leaky bucket and GCRA are the same family: the `go-redis/redis_rate` package describes itself as implementing "GCRA (aka leaky bucket)". The bucket size is the part worth thinking about, because it is the burst you are explicitly allowing.

Whichever you pick, send the headers from the first section. A limiter that returns 429 with no `Retry-After` is asking every client to guess, and the guesses will be wrong in the direction that costs you: too short, and the retry storm continues; too long, and legitimate users are locked out of a limit that lifted a minute ago.

## Don't use Redis as a rate limiter

That is the title of a Hacker News thread from August 2025 which currently sits in the top ten for `redis rate limiting`, three positions below Redis's own documentation on how to do it. A Medium series called "Why you shouldn't use Redis as a rate limiter" ranks between them. The official guidance and the argument against it are on the same page of results, and nobody has written the piece that resolves it.

The strongest version of the objection

From the Hacker News thread, verbatim: "Rate-Limiting is always a deployment specific feature. If it's for limiting user requests, then it should be a component of the ingress/API...". The point is that a limiter which only triggers after a request has reached your application has already cost you the connection, the routing and a round trip to Redis, which is the wrong place to absorb an attack.

That is correct, and it is an argument about one layer rather than about Redis. Rate limiting happens at more than one level, and the levels want different tools.

| Level                    | What it can see                                                    | What it is good at                                           | What it cannot do                                            |
| ------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| CDN or edge              | IP, path, headers                                                  | Absorbing floods before they cost you anything               | Knowing who the user is, or what their plan entitles them to |
| Ingress or reverse proxy | IP, path, sometimes an API key                                     | Cheap coarse limits close to the door                        | Per-tenant quotas that depend on your database               |
| Application              | The authenticated user, the tenant, the plan, the cost of the call | Exactly the limits you sell and the ones you owe an upstream | Stopping traffic before it arrives                           |

The objection is right about the first two rows and does not apply to the third. Coarse limits belong at the edge because they are cheap there; precise ones belong in the application because only the application knows what the numbers mean.

Redis belongs to the third row, and the reason is narrow: the counter has to be shared. Your application runs as more than one instance, and a per-process counter gives each instance its own allowance, so a limit of 100 per minute silently becomes 100 times however many instances you happen to be running. An in-memory counter is not a rate limiter for anything horizontally scaled; it is a per-instance suggestion.

### The counter is durable data in a keyspace you treat as a cache

This is the failure worth planning for and it is quiet. Rate-limit counters usually live in the same Redis instance as the cache, because that is the Redis that already exists. They are not cache: a cache entry can be recomputed and a counter cannot. If that instance is configured with an `allkeys-lru` policy and memory gets tight, the counters are eligible for eviction along with everything else, and evicted counters mean the limit stops applying without a single error anywhere.

The general form of that trap, along with what the eight eviction policies actually do and why a mixed instance has no correct one, is in [the write-up of the OOM error this produces at the other extreme](https://runsite.app/blog/redis-oom-command-not-allowed). The short version for a limiter: counters want a TTL and an instance whose policy will not delete them, and [deciding what belongs in Redis at all](https://runsite.app/blog/when-to-use-redis-cache) is the version of this question asked early.

## Your rate limiter keeps an access log

A rate limiter works by remembering who did what and when. The keys look innocuous, and they are records about people.

text

```
ratelimit:ip:203.0.113.45:2026-08-28T09:14   -> 63
ratelimit:user:8241:login                    -> 4
```

An IP address is personal data under the GDPR, and a per-IP limiter is therefore a log of access times tied to an identifier, sitting in a datastore that usually has no retention policy because nobody thought of it as storage. Login attempt counters are the same thing with a sharper edge, since they record failed authentication against a named account.

Two things follow, and neither is expensive. Give every limiter key an explicit TTL, sized to the window rather than left to whatever the instance does under pressure, so the data disappears because you decided it should rather than because memory ran out. And keep the instance in a jurisdiction you can account for: [where the data physically rests and who is a processor for it](https://runsite.app/blog/gdpr-data-residency) applies to a counter exactly as it applies to a database row, and the counter is easier to forget.

A note for anyone who arrived here from biochemistry

"Rate-limiting step" is a standard term in enzyme kinetics, and on that exact phrase the biochemistry sense outranks the software one: the neighbouring searches are for the rate-limiting enzyme of glycogenolysis and the rate-limiting step of cholesterol synthesis. If that is what you were looking for, nothing on this page will help. The two fields borrowed the same words for the same underlying idea, which is the slowest stage setting the pace of everything downstream.

## How Runsite handles it

The precise, per-user half of the table above is application code, and it needs a counter that every instance of that application shares. [Managed Redis hosted in the EU](https://runsite.app/services/redis) is where that counter goes, on the same invoice as the [application itself](https://runsite.app/services/web-services), which means one network hop rather than a round trip to another provider on every limited request.

To be clear about what we do not do: there is no built-in limiter at the edge, so the coarse layer is yours to place. What the platform removes is the operational part — an instance with a memory ceiling set by the plan rather than left at a default, so the eviction problem above does not arrive by accident. Servers are in Germany as of writing and a signed GDPR data processing agreement covers every account, which is the piece the previous section is about: those counters are personal data and they are sitting somewhere specific.

Being fair about the trade: none of the algorithms in this article require a managed anything, and a single-instance application with a counter in process memory is a perfectly good limiter until the day you run two instances. Setup details are in the [Runsite docs](https://docs.runsite.app).

## The short version

* `429 is not an error`, in the words Google highlights from the twelve-year-old Stack Overflow answer that is the only developer-facing result on the whole first page. It is the server successfully telling you to slow down.
* The first page of results for this status code is written for end users: clear your cache, disable extensions, check your WordPress plugins, change your IP. Correct for them, useless for you.
* `Retry-After` is the header that joins both sides, and it takes two forms: a number of seconds or an HTTP-date. Clients need to parse both; the date form is what a limiter resetting on a clock boundary sends.
* `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` let a client slow down before it hits the wall. They are an IETF draft as of writing, and `X-RateLimit-*` remains widespread.
* When there is no header, back off exponentially with jitter. The jitter is the half that matters, because synchronised clients doubling on the same schedule return together.
* Cap the backoff and cap the attempts. Work that will never succeed belongs somewhere a human can see it, not in an infinite retry.
* Model APIs are a distinct case. Their quotas are usually two-dimensional — requests per minute and tokens per minute — so the same call count can pass and then fail because the prompts got longer. Those calls belong behind a queue.
* Three algorithms, and the difference is the window boundary: fixed window allows a double burst across it, sliding window does not, token bucket allows a deliberate burst you configured.
* "Don't use Redis as a rate limiter" is right about one layer. Coarse limits belong at the edge where they are cheap; per-user and per-tenant limits belong in the application, which is the only place that knows what the numbers mean.
* The reason the application layer needs Redis is that the counter must be shared. A per-process counter turns a limit of 100 per minute into 100 per instance.
* Limiter counters are not cache. On an instance with an `allkeys-lru` policy they can be evicted, and an evicted counter means the limit silently stops applying.
* A rate limiter is an access log. IP addresses are personal data, login counters record failed authentication, and both want an explicit TTL and a jurisdiction you can account for.

Related service

## Managed Redis on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed Redis](https://runsite.app/services/redis)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### How long to wait for 429 too many requests?

Read the response before guessing. If it carries a \`Retry-After\` header, that is the authoritative answer and it takes one of two forms: a number of seconds, or an HTTP-date giving the moment the limit lifts. Many APIs also send \`RateLimit-Reset\`, which tells you how long the current window has left. If neither is present, back off exponentially with randomness: double the wait after each attempt, then pick a random delay between zero and that value, so that clients which failed at the same moment do not all return at the same moment. Put a ceiling on the delay, because a retry scheduled an hour away is usually worse than failing cleanly, and a limit on the number of attempts, because work that cannot succeed should surface somewhere visible rather than loop. For model APIs the window is often a minute wide and measured in tokens as well as requests, so retrying after a second achieves nothing.

### How to fix 429 too many requests?

For a developer hitting someone else's API, there are three fixes and only one of them is code. First, honour \`Retry-After\` if it is sent and use exponential backoff with jitter if it is not, which resolves the transient case. Second, find the documented limit and make your consumption of it visible in your own monitoring, because the common cause is not a burst but steady growth that crossed a threshold nobody was watching. Third, move the calls that can wait out of the request path and into a queue, so a temporary refusal becomes a retry rather than a user-facing error. If instead you are seeing this in a browser on someone else's site, the usual advice applies and it is different advice: wait a few minutes, clear the site's cookies, disable extensions that might be refreshing in the background, and if you are on a shared or corporate connection, consider that the limit may be counting everyone on your IP address rather than you.

### Can I use Redis for rate limiting?

Yes, for the layer where it belongs, and the well-known objections are about a different layer. Rate limiting happens at several levels: a CDN or edge sees IP and path and is the right place to absorb floods cheaply, while your application is the only layer that knows who the authenticated user is, which tenant they belong to and what their plan entitles them to. Redis is for that second kind. The reason it is needed at all is that the counter has to be shared: an application running several instances with a counter in process memory does not enforce 100 requests per minute, it enforces 100 per instance. Implementations usually build on atomic increments with an expiry, or on a token bucket; the \`go-redis/redis\_rate\` package implements GCRA, also known as leaky bucket. The failure to plan for is that counters are durable data. Kept in a cache instance with an eviction policy that can delete them, they will be deleted under memory pressure and the limit will stop applying without any error.

### What is the difference between throttling and rate limiting?

The terms are used interchangeably often enough that you should check what a given API means by them, but the useful distinction is about what happens to the excess request. Rate limiting rejects it: you are over the quota, so the request gets a 429 and does not run. Throttling slows it down: the request is accepted and queued or delayed so that it completes at a rate the system can sustain, which is why throttling is often invisible as latency rather than as an error. Rejecting is the right behaviour for a public API, where an honest refusal lets the client back off intelligently and a hidden delay just moves the queue into somebody's connection pool. Delaying suits internal traffic where the work must eventually happen and the caller can afford to wait. Some systems do both at once, throttling up to a threshold and rejecting past it, which is effectively what a token bucket with a queue in front of it produces.

Keep reading

---

# Redis vs Database for Sessions: Which to Choose

> Sessions leave process memory once you run two instances. What Redis and Postgres each do about expiry, logout, restarts and backups.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Redis vs Database for Sessions: What Actually Differs

CachingSeptember 9, 202616 min read

# Redis vs Database for Sessions: What Actually Differs

Every result on the first page compares speed. The choice is made by four other things: who deletes expired sessions, how you log someone out everywhere, what a restart costs, and how long the copies live.

## The short version

Speed is not what decides this. Four other things do: who deletes expired sessions, since a database table needs a purge job, how you log someone out everywhere, what a restart or an eviction costs you, and how long the copies live — because a session table sits in your backups long after the session ended.

[Teo Marquardt](https://runsite.app/about#author)

The question usually shows up on the day an application gets a second instance. A user logs in, the next request lands on the other container, and they are logged out; one refresh later they are back in. Or it shows up as three lines in a production log that nobody on the team wrote.

Search for the comparison and the first page, as of writing, is an argument about speed. A Reddit thread, a fourteen-year-old Stack Overflow question from someone moving their sessions off MySQL because it felt slow, a 2019 benchmark from a German hosting company, several posts explaining that memory is faster than disk. All of it is true, and it is rarely what decides the matter.

What decides Redis vs database for sessions is who deletes a session once it has expired, how you log a user out of every device, what a restart or memory pressure does to people who are logged in, and how long copies of a session outlive the session itself. The two stores give opposite answers to most of those, which is why the choice is less obvious than the benchmarks make it look.

## MemoryStore is not designed for a production environment

Those three log lines are the Node.js version of the problem. `express-session` prints them when `NODE_ENV` is `production` and nobody configured a store:

text

```
Warning: connect.session() MemoryStore is not
designed for a production environment, as it will leak
memory, and will not scale past a single process.
```

The package's README is blunter still: the default store "is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing."

Two questions hide in that warning, and they are worth separating. The first is whether sessions live inside the application process, and for anything running more than one instance the answer has to be no. The second is where they live instead, which is the actual Redis-or-database decision. Most frameworks have already answered the first for you, and they did not all answer it the same way.

| Framework                 | Default session storage                                | With a second instance                                                                       |
| ------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Express (express-session) | MemoryStore, inside the Node.js process                | Each instance has its own sessions, and the warning above is printed in production           |
| Django                    | Database, django.contrib.sessions.backends.db          | Works across instances; expired rows stay until clearsessions runs                           |
| Laravel                   | Database, SESSION\_DRIVER=database                     | Works across instances; expired rows are swept on a lottery                                  |
| Rails                     | Encrypted cookie, ActionDispatch::Session::CookieStore | Works across instances with no server-side state, a 4 kB limit and no server-side revocation |

Defaults as of writing. Laravel's default has changed between major versions, so check `config/session.php` in your own project rather than a tutorial.

Read the table the other way round and it says something useful. Two of these four frameworks already keep sessions in your relational database out of the box, so for a lot of teams the practical question is whether to move off the store they already have.

## Sticky sessions: the answer that postpones the question

Before choosing a store, many teams choose not to. Sticky sessions, also called session affinity, tell the load balancer to send every request from a given user to the same instance, so in-process sessions keep working with more than one container.

That holds until the instance goes away, which on a modern platform is routine rather than an incident. A deploy replaces every container, scaling down removes some, and a failed health check moves traffic elsewhere. Each of those logs out everyone who was pinned to the instance that left, and a [rolling or blue-green release](https://runsite.app/blog/blue-green-vs-rolling-deployments) does it to the whole user base on every deploy. Affinity also skews load, because a busy user stays on one instance however loaded it gets.

The generated answer at the top of Google's results for the term ends, as of writing, by offering to help you "decide whether sticky sessions or a shared session store fits your project better". That is the right framing, and it is left open. For login state, affinity defers the store decision until the first deploy makes it for you. It has honest uses elsewhere, such as keeping a reconnecting WebSocket on a warm instance, but as a home for sessions it only moves the problem.

## Redis vs database for sessions: what actually differs

Put the same session in both stores and compare what happens to it over its life, rather than how long one read takes.

|                     | Database table                                                     | Redis                                                                                        |
| ------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Reading the session | An indexed lookup by primary key, over a pooled connection         | A key lookup in memory                                                                       |
| Writing it back     | A row update: a new row version, a WAL record, work for autovacuum | An in-memory write; persistence, if enabled, happens in the background                       |
| Expiry              | Nothing deletes expired rows until a sweeper runs                  | A TTL on the key, and Redis deletes it                                                       |
| Sign out everywhere | One DELETE, if the user ID is a column                             | Needs a per-user index you maintain                                                          |
| Restart             | Nobody is logged out                                               | Sessions created since the last persisted point are lost, or all of them without persistence |
| Memory pressure     | Not a session concern                                              | Failed logins or random logouts, depending on the eviction policy                            |
| Backups             | Copied into every backup for the retention window                  | Rarely backed up; snapshot files hold only live sessions                                     |

Rows three to seven decide it. Row one is the only one the benchmarks on the first page measure.

### Django does not provide automatic purging of expired sessions

Django's documentation says it in those words, and the sentence holds for every relational session store, not only Django's. A row in a sessions table with an expiry date in the past is still a row: it is ignored when read and stays on disk until something deletes it. Django's answer is a management command, `clearsessions`, which the docs recommend running "on a regular basis, for example as a daily cron job". Laravel's answer is a lottery, in which a small share of ordinary requests sweeps expired sessions while serving the page.

text

```
# Laravel: .env
SESSION_DRIVER=database    # the default in current releases
SESSION_LIFETIME=120       # minutes

# Laravel: config/session.php
'lottery' => [2, 100],     // "By default, the odds are 2 out of 100."

# Django: nothing deletes expired rows until this runs, e.g. from crontab
0 3 * * * python manage.py clearsessions
```

The `connect-pg-simple` store for Express runs a timer of its own, pruning expired sessions every 900 seconds by default. The mechanisms differ and the failure is the same everywhere: a sweeper that was never scheduled, or one switched off for being slow on a table that had already grown to millions of rows. A sessions table without a working sweeper is an append-only log of every visit that ever created a session.

Redis has no equivalent job, because expiry is a property of the key. Set a TTL equal to the session lifetime and Redis removes the key when it lapses; `connect-redis` takes that TTL from the cookie's expiry and falls back to one day. Laravel's session handler interface even notes that for self-expiring stores such as Redis the garbage-collection method "may be left empty". If sessions stay in the database, the sweep is a scheduled job like any other and belongs wherever [the rest of your scheduled work](https://runsite.app/blog/cron-jobs-workers-queues) runs.

### Logging someone out everywhere

"Sign out of all devices", a password reset and a suspended account all need every session belonging to one user to stop working immediately. OWASP's session management guidance is explicit that the application has to invalidate sessions on the server side. Deleting a cookie in one browser does nothing to the other five.

In a relational table that is a single statement, provided the user ID is a column. Laravel's default `sessions` table has one, and it is indexed. Django's `django_session` table does not: it holds a session key, encoded session data and an expiry date, so finding one user's sessions means decoding rows or keeping a mapping of your own.

Redis finds a session by its ID and by nothing else, and the common session libraries store exactly that. Logging a user out everywhere means maintaining a second structure yourself, typically a set of session IDs per user.

text

```
-- Database: one statement, if user_id is a column (Laravel's default table)
DELETE FROM sessions WHERE user_id = 42;

# Redis: record the session in a per-user set when it is created...
SADD user:42:sessions sess:9f21c4a7
EXPIRE user:42:sessions 1209600

# ...and walk the set on "sign out everywhere"
SMEMBERS user:42:sessions
DEL sess:9f21c4a7 sess:c03be118 user:42:sessions
```

The set outlives members that expired on their own, so deleting a key that is already gone is normal and harmless. What is not harmless is skipping the index and discovering, on the day someone reports a stolen laptop, that the only way to find their sessions is to scan every key and deserialise every value.

### What a restart and an eviction do to each

A database restart logs nobody out. A Redis restart loses every session created after the last point Redis persisted, and all of them if persistence is off. With snapshots alone that can be several minutes of logins; with an append-only file flushed every second, it is typically about a second. Whether a restart costs your users anything is a configuration question, and it is better answered before the first restart answers it for you.

Memory pressure is the quieter failure, and the eviction policy decides its shape. Under `noeviction` a full instance refuses writes, so new logins fail with `OOM command not allowed`. Under `allkeys-lru` sessions are deleted alongside cache entries and users are logged out at random. The `volatile-*` policies are worse than they sound for sessions: they consider only keys with a TTL, sessions always have one, and a cache written without expiry times is never touched, so the sessions are what goes. [Why a keyspace that is half cache and half store has no correct policy](https://runsite.app/blog/redis-oom-command-not-allowed) is the long version. The short one is that sessions want an instance, or at least a memory budget, where eviction is not part of the plan.

There is a middle option on the Postgres side, worth a mention because it looks like a free lunch. An `UNLOGGED` table skips the write-ahead log, which makes writes to it considerably faster.

sql

```
CREATE UNLOGGED TABLE sessions (
  id          text PRIMARY KEY,
  user_id     bigint,
  payload     jsonb NOT NULL,
  expires_at  timestamptz NOT NULL
);

CREATE INDEX ON sessions (user_id);
CREATE INDEX ON sessions (expires_at);
```

The PostgreSQL documentation states the price plainly: an unlogged table "is automatically truncated after a crash or unclean shutdown", and its contents "are also not replicated to standby servers". On a managed database with a standby, a failover promotes a replica whose sessions table is empty. You have rebuilt the behaviour of a Redis restart inside Postgres, minus the TTL.

## When the database becomes the wrong place

Reading a session by primary key is cheap for any relational database. Most of the cost is the network round trip, and Redis pays a round trip too. If reads were the only difference, a database-backed session store would last far longer than most teams assume.

Writes are where it changes, because many frameworks write the session back on every request rather than only when something in it changed. In current Laravel releases the session middleware saves the session at the end of each stateful request, and `express-session` stores that implement `touch` update the expiry on activity. Django is the exception: it writes only when the session was modified, unless `SESSION_SAVE_EVERY_REQUEST` is switched on.

In PostgreSQL each of those updates is a new row version, a WAL record and a dead tuple for autovacuum, on a table whose working set is small and whose churn is total. Each one also holds a connection while it runs, which is where session traffic meets [the connection ceiling a pooler exists to protect](https://runsite.app/blog/pgbouncer-connection-pooling). The signal that sessions should leave the database shows up on the write side: session updates among your most frequent statements, a sessions table that bloats faster than vacuum reclaims it, or a connection pool whose busiest clients are page views that changed nothing.

About the benchmark on the first page

The most visible number for this comparison in the German results comes from maxcluster, a hosting company, published on 26 September 2019: "Redis achieves a 33% higher throughput in read operations than the NVMe SSDs and almost three times higher than MySQL." It is a real measurement of read throughput on their hardware at the time. It says nothing about expiry, revocation or restarts, and at the traffic most applications see, read throughput is not the limit that arrives first.

## Redis vs database for sessions vs cache: running both

Storing sessions in the database and Redis at once is a real option, and Django ships it. Django's `cached_db` session backend, in the documentation's words, "uses a write-through cache – session writes are applied to both the database and cache, in that order". Reads come from the cache and fall back to the database when the key is missing.

python

```
# settings.py
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "rediss://default:password@cache.example.eu:6379",
    }
}
```

This keeps the database as the record and Redis as the fast path. A flushed or restarted Redis costs a round of cache misses rather than a round of logouts, and signing someone out everywhere still happens against rows. The price is that every session write touches both stores, and the sweeper is still yours to schedule. It is the [write-through pattern](https://runsite.app/blog/cache-aside-write-through-write-behind) applied to a single table, with the same trade of consistency paid for on every write. Most other frameworks leave the hybrid to you, and the order is the part to copy: database first, cache second.

Django's docs attach a warning that applies to every framework. Cache-backed sessions should only use Memcached or Redis, because the local-memory cache backend "is NOT multi-process safe". That is the MemoryStore problem again under a different name.

## The option with no server-side store at all

Rails takes a third route by default. Its `CookieStore` puts the whole session, encrypted and signed, into the cookie, so there is no store to choose and nothing to share between instances. That scales without effort and costs two things. One is size, and the Rails security guide puts it directly: "Cookies have a size limit of 4 kB. Use cookies only for data which is relevant for the session." The other is control: "Session cookies do not invalidate themselves and can be maliciously reused."

Signed tokens of every kind, JWTs included, share that second property. A token the server cannot recall is valid until it expires, so logging a user out everywhere needs a denylist or a per-user version number checked on each request. Both are server-side state read on every request, which brings you straight back to the question this article is about, only with a smaller record to store.

## Your session table is in your backups

The case that sessions are personal data has already been made [for the cache in front of your database](https://runsite.app/blog/when-to-use-redis-cache): session tokens and user identifiers fall under the GDPR however temporary they feel. Choosing a store adds a question that argument does not reach, which is how long the copies live.

A sessions table in PostgreSQL is copied into every backup and every archived WAL segment for as long as the retention window lasts. Laravel's default table stores `ip_address` and `user_agent` next to `user_id`, so those copies are a record of which device, from which address, used which account, kept for days or weeks after each session ended. Deleting the row at logout removes it from the live database and from nothing else.

A Redis session with a TTL leaves no such trail. Snapshot and append-only files contain the sessions that were live when they were written, and Redis is rarely the component anyone keeps thirty days of backups for. The GDPR's storage limitation principle in Article 5(1)(e) asks that personal data be kept in identifiable form for no longer than necessary, and that is an easier argument to make for a key that disappears with the session than for a row restored with every backup.

None of this rules the database out. It means the retention of your backups becomes the retention of your session data, which is worth writing down in your record of processing, and where those copies physically sit is part of [the same residency answer as the database itself](https://runsite.app/blog/gdpr-data-residency).

## Which one to pick

* **Stay in the database** if you already have one, run a handful of instances and need to query sessions: a list of active devices, sign-out everywhere, an audit trail. Schedule the sweeper on day one and count the sessions table in your backup retention.
* **Move sessions to Redis** when session writes show up in database load or in the connection pool, or when you already run Redis and sessions are its next obvious tenant. Set the TTL to the session lifetime, keep a per-user set if you need revocation, and put the keys where eviction will not reach them.
* **Run both** when you want the database as the record and Redis taking the reads. Django's `cached_db` does this out of the box, and any framework can do it by writing to the database first and the cache second.
* **Use signed cookies** when the session is a couple of identifiers, fits comfortably under 4 kB, and revocation through a denylist is acceptable.
* **Do not treat sticky sessions as a store.** They keep in-process sessions alive until the next deploy, which on most platforms is this week.

If you were looking for a music shop in Frankfurt

In Germany the most searched phrase containing the words "session store" is `session the music store frankfurt`, at around 1,900 searches a month against about 90 for the software term. Nothing on this page will help with guitars, although the servers mentioned below are at least in the same country.

## How Runsite handles it

Both stores in this article run as toggles next to your application, on the same invoice. [Managed Redis in Germany](https://runsite.app/services/redis) is Redis 7+ with TLS on by default and, as of writing, persists with RDB snapshots and AOF on every plan, with automatic failover, so a restart or a failover does not log your users out. It is not included in backups, which for sessions is the behaviour the previous section argues for. The eviction question stays with you: size the instance for its sessions rather than sharing a tight memory budget with a cache.

[Managed PostgreSQL with daily backups and point-in-time recovery](https://runsite.app/services/postgresql) runs PostgreSQL 16, keeps backups for seven days on the entry plans and up to thirty on larger ones, and has PgBouncer built in for the connection side of session traffic. If sessions stay in the database, that retention window is the one to record against them, and the sweeper can run as a [scheduled job](https://runsite.app/services/cron-jobs) on the same platform.

Instances, RDB and AOF files and database backups all stay in Germany as of writing, and a signed GDPR data processing agreement comes with every account. What we do not provide is a session layer: which store, which TTL and how revocation works are decisions for your application, and the setup steps are in the [Runsite docs](https://docs.runsite.app).

## The short version

* The question is really two: whether sessions leave the application process, which they must once you run two instances, and where they go instead.
* `express-session` warns that `MemoryStore is not designed for a production environment` for a reason. Django and current Laravel already keep sessions in the database by default, and Rails keeps them in an encrypted cookie.
* Sticky sessions postpone the decision until a deploy, a scale-down or a crash logs out everyone pinned to the instance that left.
* `Django does not provide automatic purging of expired sessions`, and no relational store does. Django needs `clearsessions` on a schedule, Laravel sweeps on a 2-in-100 lottery, and Redis deletes a key when its TTL lapses.
* Signing a user out everywhere is one `DELETE` on a table with a `user_id` column. In Redis it needs a per-user set that you maintain.
* A Redis restart loses sessions written since the last persisted point. Under memory pressure `noeviction` breaks logins, `allkeys-lru` logs people out at random, and `volatile-*` evicts sessions first when cache keys carry no TTL.
* An `UNLOGGED` Postgres table is faster to write, and a crash or a failover to a standby empties it.
* The database becomes the wrong place when session writes, not reads, show up in load, bloat or the connection pool. Laravel writes the session on every request; Django only when it changed.
* Django's `cached_db` runs both stores as a write-through cache, with the database as the record.
* Signed cookies and JWTs remove the store and the ability to revoke. Getting revocation back means server-side state again.
* A sessions table sits in every backup for the whole retention window, often with IP addresses and user agents attached. A Redis session with a TTL leaves no such copies.

Related service

## Managed Redis on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed Redis](https://runsite.app/services/redis)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Can Redis be used as a session store?

Yes, and it is one of the most common things Redis is used for. A session is a small value looked up by ID on almost every request and deleted when it expires, and Redis handles that with one key per session and a TTL equal to the session lifetime, so expired sessions are removed without a cleanup job. Most frameworks have a ready integration, including \`connect-redis\` for Express, Django's cache and \`cached\_db\` session backends, and Laravel's \`redis\` driver. Three things need deciding before production. Persistence: without RDB snapshots or an append-only file, a restart logs everyone out. Eviction: on an instance that doubles as a cache, a full memory budget either blocks new logins or deletes sessions, depending on the policy. Revocation: Redis finds sessions by ID only, so signing a user out of every device needs a per-user set of session IDs that you maintain yourself.

### Where is session data stored?

In most web applications the browser holds only a session ID in a cookie, and the data itself lives on the server in a session store. Which store depends on the framework and its configuration. As of writing, Django and Laravel default to a table in your relational database, Express's \`express-session\` defaults to process memory, which its own documentation says is not meant for production, and Rails defaults to keeping the whole session encrypted inside the cookie, so nothing is stored on the server at all. Redis and Memcached are the usual alternatives when sessions move out of the database. The choice starts to matter as soon as an application runs more than one instance, because every instance has to read sessions created by any other. That rules out process memory and turns the session store into a shared component with its own expiry, backup and data protection questions.

### Why is Redis faster than a database?

Mainly because Redis keeps its data in memory and does far less work per operation. A relational database is built to guarantee durability and consistency across arbitrary queries: it parses and plans SQL, manages row versions and locks, writes to a write-ahead log before confirming a change, and reads from disk whenever a page is not already cached. Redis looks up a key in an in-memory structure and returns the value. For reading a single session by primary key the gap is smaller than general comparisons suggest, since a database keeps frequently used pages in memory and much of the time goes on the network round trip, which Redis pays as well. The difference is larger for writes. An update in PostgreSQL creates a new row version and a WAL record, while Redis changes memory and persists in the background, if it persists at all. That is why session writes, rather than reads, usually push sessions out of a database.

### Can Redis store data permanently?

It can persist data to disk, but it is not designed to be a permanent system of record. Redis has two persistence mechanisms: RDB snapshots written at intervals, and an append-only file (AOF) that logs every write and is commonly flushed to disk once per second. With AOF enabled a crash typically loses about a second of writes; with snapshots alone it loses everything since the last snapshot. Either way the data survives a restart, but Redis still holds the whole dataset in memory, so capacity is bounded by RAM, and an eviction policy can delete keys under memory pressure unless it is set not to. For sessions that trade is the right one: surviving a restart matters, keeping data forever does not, and a TTL removes each session when it ends. Anything you cannot recompute or afford to lose belongs in a database with backups, with Redis in front of it where speed matters.

Keep reading

---

# Cron Job on Multiple Instances: Why It Runs Twice

> An in-app scheduler fires once per replica, goes silent when the instance sleeps and doubles during a rolling deploy. Where the schedule belongs instead.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Cron Jobs on Multiple Instances: Why Your Scheduled Task Runs Twice

SchedulingSeptember 11, 202614 min read

# Cron Jobs on Multiple Instances: Why Your Scheduled Task Runs Twice

A scheduler inside your app is a timer in memory, and every replica has its own. What that does under autoscaling, rolling deploys and sleeping instances, and where the schedule should live instead.

## The short version

A scheduler inside your app is a timer in memory, and every replica has its own, so the job runs once per instance. Autoscaling, rolling deploys and the idle side of a blue-green release all change the replica count without telling you. A lock skips rather than catches up; the schedule belongs outside the app.

[Teo Marquardt](https://runsite.app/about#author)

The weekly report went out three times. Same recipients, same numbers, three emails a few milliseconds apart. Nobody had touched the schedule. What had changed, a week earlier, was the instance count on the web service: from one to three.

It is an old problem that keeps arriving in new frameworks. For the Node.js version, Google still shows a Stack Overflow question from thirteen years ago titled "Nodejs cron plugin vs running nodejs script from crontab", with a single answer. The same problem shows up today as the related search "Nestjs/schedule multiple instances", and as a Rails thread that opens with "A doubt that woke me up last night".

This article is about where a schedule lives, not which primitive to use. If you are still choosing between a cron job, a background worker and a queue, [the difference between the three and how each one fails](https://runsite.app/blog/cron-jobs-workers-queues) is covered separately. Here the work is already a scheduled task, the application already runs on more than one instance or on one that sometimes sleeps, and the task is running the wrong number of times.

## Cron running on multiple server instance

The heading is a Stack Overflow question, left exactly as it was typed, and as of writing it is the second result Google returns for this problem. The mechanism behind it fits in one sentence: a scheduler that runs inside your application is a timer held in that process's memory, and every copy of the process holds its own.

Libraries such as `node-cron`, `@nestjs/schedule` and APScheduler do not coordinate across processes by default. Each one reads the schedule from your code at startup, works out the next fire time and waits. Start three containers from the same image and you have three timers aimed at the same minute. None of them is misbehaving. Each one does what the single copy on your laptop did.

An answer on r/node, which Google quotes in its snippet for the Node comparison, states the other half plainly: in-process scheduling means "scheduled jobs will only fire as long as your script is running". The number of executions follows the number of running processes in both directions. More processes, more runs. No process, no run.

| Stack                    | Where the scheduler runs                             | With three instances, by default                            | What the project documents                                                |
| ------------------------ | ---------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------- |
| Node.js: node-cron       | Inside the app process                               | Three runs                                                  | Version 4 adds distributed: true, so one instance executes each fire      |
| NestJS: @nestjs/schedule | Inside the app process                               | Three runs                                                  | Nothing on multiple instances; intervals use setInterval() under the hood |
| Python: APScheduler 3.x  | Inside the app or a worker process                   | Three runs, or missed runs if they share a job store        | Sharing a job store between processes: "You can't."                       |
| Python: Celery beat      | A separate beat process, or inside a worker with \-B | One run with a single beat; one per worker started with \-B | Only a single scheduler per schedule; \-B not recommended for production  |
| PHP: Laravel scheduler   | schedule:run on every server that has the cron entry | Three runs, unless the task uses onOneServer()              | "generate the report three times. Not good!"                              |
| Ruby: Solid Queue        | A scheduler process                                  | One enqueue per task and time, even with several schedulers | A unique index on task\_key and run\_at                                   |

As of writing: node-cron 4, APScheduler 3.x, Celery 5.6, Laravel 12, current Solid Queue. The third column is the one to read. It says whether the default on three instances is one run or three.

The Celery documentation, verbatim

"You have to ensure only a single scheduler is running for a schedule at a time, otherwise you'd end up with duplicate tasks." The same page calls running beat inside a worker with `-B` convenient "if you'll never run more than one worker node", and not recommended for production. Scale that worker service to two and the embedded schedulers become two.

Laravel's documentation makes the point with a worked example: a report scheduled for Friday evening, a scheduler running on three worker servers, and the result, in its own words, "the scheduled task will run on all three servers and generate the report three times. Not good!"

Two different kinds of "running twice"

This article is about one scheduled moment firing on several machines at once. A different problem produces the same symptom: a run that is still going when the next scheduled run starts, on the same machine. That one is handled by an overlap policy (skip the new run, allow both, or replace the old one) and has an article of its own coming. The two need separate fixes. An overlap setting decides what happens when a run meets the previous run; it says nothing about how many machines started one.

## Three places a schedule can live

Strip the frameworks away and a schedule can sit in one of three places. They part ways on the events that cause trouble: more instances, no instances, and a deploy.

| Where the schedule lives                                                                                                  | Runs per fire with N instances                     | Instance asleep or scaled to zero      | During a deploy                                       | What stays with you                                            |
| ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------- |
| Inside the web process (node-cron, @nestjs/schedule, APScheduler in the app)                                              | N                                                  | Nothing fires                          | Old and new processes can both fire                   | Deduplication, every time the instance count changes           |
| In one dedicated always-on process (a single Celery beat, a standalone scheduler)                                         | One, while there is exactly one                    | Fires, if that process is kept running | Briefly two, depending on how the process is replaced | Keeping the count at exactly one, and noticing when it is zero |
| Outside every app process, as a job started on schedule (a crontab on one host, a Kubernetes CronJob, a managed cron job) | One per schedule, whatever the app's replica count | Fires; it starts its own container     | Unaffected by the app's rollout                       | Idempotency, because "once" is still approximate               |

The middle row looks safest and asks for the most discipline. A single scheduler is correct until the day there are two of them, or none.

The middle row is what most people mean by an always-on worker, or what people shopping for hosting describe as "persistent background processes". It fixes the multiplication by moving the schedule out of the part that scales. It does not remove the need for a count of exactly one; it concentrates that need in a single place you can watch. [Web services that autoscale from one instance to many](https://runsite.app/services/web-services) are the right home for request-handling code and the wrong home for the timer.

If you searched for BackgroundWorker

"Background worker" is a crowded phrase. In Google's German results, as of writing, much of the search around it concerns the .NET `BackgroundWorker` class, which moves work off a desktop UI thread, or browser Service Workers and background sync. Neither is a server process, and neither has a schedule that multiplies across instances. The always-on worker in this article is a long-running server process deployed next to your application.

## The replica count changes when you are not looking

If the instance count were a number you set once, the fix would be to set it to one and move on. In practice it changes for reasons that have nothing to do with the schedule.

### Autoscaling

Horizontal autoscaling adds instances when CPU or memory crosses a threshold and removes them when load falls. A schedule inside that service now fires a different number of times depending on traffic at the scheduled minute. The job at 03:00 runs once, because the service has scaled down to one. The job at 13:00 runs four times, because lunchtime traffic scaled it to four. Both pass in testing, and the bug report reads like an intermittent fault that nobody can reproduce.

### A rolling deploy runs two of everything

A rolling release starts the new containers before it stops the old ones, so that traffic always has somewhere to go. [For a stretch of seconds during a push-to-deploy release, both versions of your application exist](https://runsite.app/blog/git-push-to-deploy), and each has its own scheduler loaded. If the rollout straddles a scheduled minute, a service running a single replica fires twice. This is the version that catches teams who already know about multiple instances and deliberately run one.

### The idle environment in blue-green is not idle

Blue-green keeps a complete second copy of the application beside the live one, ready to take traffic. The router decides which copy receives requests. Nothing decides which copy runs its timers, because a scheduler does not wait for traffic. [How blue-green and rolling deployments differ in what runs side by side](https://runsite.app/blog/blue-green-vs-rolling-deployments) is usually discussed in terms of requests and schema changes; timers belong on the same list. Unless the idle environment starts with its scheduler switched off, both colours send the Friday report.

### Set Interval in Node.js vs. Cron Job?

This Stack Overflow question dates from 2013 and still sits in Google's discussions block for the Node comparison. The failure it leads to runs the other way from everything above. Too few runs, often zero.

javascript

```
// Fires 24 hours after this process started, not at a time of day.
// Any restart or deploy before then starts the countdown again.
setInterval(sendDailyDigest, 24 * 60 * 60 * 1000);
```

That line does not mean "once a day". It means "24 hours after this process started, and every 24 hours after that, for as long as the process lives". Every restart resets the countdown, so an application deployed more often than once a day never reaches the first tick and the digest silently never goes out. Cron-expression schedulers such as `node-cron` avoid the reset, because they compute the next wall-clock time instead of counting from startup. They share the second problem, though: a process that is not running has no timer at all.

Sleeping instances make it worse. A host that [spins a container down after a stretch without traffic](https://runsite.app/blog/cold-starts) stops the process, and the timer goes with it. Nothing wakes the container at 03:00, because waking it takes a request, and the schedule was the thing that was supposed to produce one. A schedule that depends on the process being awake inherits every reason the process might not be.

## Locks, unique rows or one scheduler: what each fix leaves with you

They are not interchangeable. Each one removes a failure mode and quietly hands you a different one.

### A lock skips, it does not catch up

The most common fix keeps the scheduler in every instance and makes them race for a lock in a shared store. Whoever takes the lock runs the job; everyone else stands down. ShedLock, a lock library for Spring's scheduled tasks, describes itself in one line: "ShedLock makes sure that your scheduled tasks are executed at most once at the same time." As of writing, Laravel's version is a single method, and it needs a cache store that every server shares:

php

```
Schedule::command('report:generate')
    ->fridays()
    ->at('17:00')
    ->onOneServer();
```

`node-cron` added the same idea in version 4\. As of writing, the option uses an environment-variable flag out of the box to mark which instance runs, and can use a Redis coordinator when that one instance should not be a single point of failure:

javascript

```
cron.schedule('0 17 * * 5', generateWeeklyReport, {
  name: 'weekly-report',
  distributed: true,
});
```

The sentence to notice in ShedLock's documentation comes further down. If a task is already running on one node, "execution on other nodes does not wait, it is simply skipped." Right for preventing duplicates, and wrong in the one case that hurts: the node holding the lock dies halfway through the job. The others skipped. Nobody retries. A lock turns "ran three times" into "ran at most once", and at most once includes zero.

Where the lock lives matters as well. All three of these can keep it in Redis, and a lock key is normally written with an expiry so that a crashed holder cannot block the job forever. Under a `volatile-*` eviction policy, keys with an expiry are precisely the keys Redis may remove when memory runs short. [When Redis reaches maxmemory and starts evicting](https://runsite.app/blog/redis-oom-command-not-allowed), a lock that vanishes early lets a second instance start the job while the first is still running it. If a lock guards anything expensive, keep it on a Redis instance that is not also a cache under memory pressure, or on one set to `noeviction`.

### A unique row per run

A sturdier variant stops treating duplicates as a race and treats them as a constraint. Rails' Solid Queue lets several schedulers run the same recurring configuration, for redundancy, and stops duplicates in the database: each enqueued run writes a row to a table with "a unique index on `task_key` and `run_at`, ensuring only one entry per task per time will be created." Two schedulers can both try, and the second insert fails. As of writing, the guarantee holds as long as finished jobs are kept, which is the default.

The difference from a lock is that the record outlives the attempt. A lock expires, and after it expires nothing remembers that the 17:00 run happened. A row keyed by task and scheduled time is still there at 17:01\. That is what makes the approach worth copying into any application with a relational database, whatever scheduler it uses, which the section on idempotency below does.

### How do I share a single job store among one or more worker processes?

APScheduler's FAQ asks this in so many words, and the documented short answer is "You can't." The long answer explains that sharing a persistent job store between processes "will lead to incorrect scheduler behavior like duplicate execution or the scheduler missing jobs". The workaround the project gives for the 3.x series is to run the scheduler as one dedicated process and have the rest of the application talk to it.

The third fix follows from it: take the schedule out of the replicated service and give it a single home. Celery's standalone `beat` is the same design. It works, and its price is the middle row of the earlier table. One scheduler is a single point of failure, and keeping the count at exactly one through restarts and deploys becomes your job instead of the framework's. It is also the point where it is fair to ask why the scheduler is a process you run at all, rather than [a scheduled job that exists once, however many replicas are running](https://runsite.app/services/cron-jobs).

## Even outside the app, it is "approximately once"

Moving the schedule out of the application removes the multiplication by replica count. It does not buy exactly-once execution, and Kubernetes says as much in its own CronJob documentation: "A CronJob creates a Job object approximately once per execution time of its schedule." It goes on to say that "there are certain circumstances where two Jobs might be created, or no Job might be created", and concludes: "Therefore, the Jobs that you define should be idempotent."

Every fix above reduces the number of duplicates, and none of them guarantees zero. What makes the remaining duplicates harmless is the job refusing to do the same work twice, keyed on its name plus the time it was scheduled for:

sql

```
CREATE TABLE scheduled_runs (
  task          text        NOT NULL,
  scheduled_for timestamptz NOT NULL,
  started_at    timestamptz NOT NULL DEFAULT now(),
  finished_at   timestamptz,
  PRIMARY KEY (task, scheduled_for)
);

-- First statement of the job. No row returned means another copy already claimed this run.
INSERT INTO scheduled_runs (task, scheduled_for)
VALUES ('weekly-report', '2026-09-11 17:00:00+02')
ON CONFLICT DO NOTHING
RETURNING task;
```

The job works out which scheduled time it belongs to, rounded to the schedule rather than read off the clock, tries to claim it, and exits quietly if the claim returns nothing. Three instances firing at once produce one row and two clean exits. A double fire during a rolling deploy is caught the same way, and so is a platform scheduler's occasional second run. The job sets `finished_at` when it completes, so a row where that column is still empty an hour later is both the sign that a claimed run died and the condition to alert on. A deploy that stops the container mid-job is one way that happens, and a job that ignores SIGTERM [shows up as exit code 137](https://runsite.app/blog/exit-code-137).

## How Runsite handles it

Both of the safe shapes above are available. [Run the schedule as a Runsite cron job instead of inside the app](https://runsite.app/services/cron-jobs): each job is defined once and starts a fresh container from your image at its scheduled time, as a separate resource from your web service, so scaling the web service from one instance to four does not change how many containers the schedule starts. Every run is recorded with its exit code, output and duration, and a failed or timed-out run can alert you by email or webhook, which covers the zero-runs case a lock never sees.

[Web services](https://runsite.app/services/web-services) are where the request-handling code belongs. As of writing, on Pro and Business plans they can autoscale horizontally between a minimum and maximum instance count, which is the setting that makes an in-app scheduler unpredictable, so the working rule is to keep timers out of any service with autoscaling switched on. On sleep, the entry Nano plan suspends a web service after 14 days without a request, and Starter and above do not sleep. A timer left inside a Nano service stops firing once the service has been quiet for two weeks.

If a schedule has to stay inside the application for now, its lock needs a shared store, and [managed Redis](https://runsite.app/services/redis) in the same region is the usual place; just keep lock keys off an instance that evicts. The idempotency table belongs in [managed PostgreSQL](https://runsite.app/services/postgresql). All of it runs in Germany, as of writing, with a signed GDPR data processing agreement included with every account. A complete example of a job that belongs outside the app is [the scheduled PostgreSQL backup to S3](https://runsite.app/blog/postgresql-backup-to-s3), and setup details are in the [Runsite docs](https://docs.runsite.app).

## The short version

* A scheduler inside your application is a timer in that process's memory. Three instances hold three timers, and each one fires.
* By default, `node-cron`, `@nestjs/schedule`, APScheduler and Laravel's scheduler multiply with the instance count. Celery beat and Solid Queue are built around a separate scheduler process.
* The instance count changes without you: autoscaling at the scheduled minute, both versions alive during a rolling deploy, and the idle side of blue-green running its own timers.
* The opposite failure is silence. `setInterval` counts from process start and resets on every deploy, and a sleeping instance has no timer at all.
* A lock prevents duplicates by skipping. If the holder dies mid-run nothing retries, and a lock key with a TTL is exactly what a `volatile-*` Redis policy evicts.
* A unique row per task and scheduled time outlives the attempt, which a lock does not.
* One dedicated scheduler process fixes the multiplication and makes you responsible for there being exactly one of it.
* Even Kubernetes promises only "approximately once". Moving the schedule out of the app reduces duplicates; an idempotency key on task plus scheduled time makes the rest harmless.

Related service

## Cron Jobs on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Cron Jobs](https://runsite.app/services/cron-jobs)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is node cron used for?

\`node-cron\` schedules functions inside a running Node.js process using cron expressions, so a task can run every five minutes or every weekday at 09:00 without an external scheduler. It is commonly used for cleanup tasks, cache refreshes, digest emails and polling another service. Because it lives inside the process, two properties matter before production. Tasks run only while that process is running, so a restart, a crash or a sleeping container means no run. And every process that loads the schedule fires it independently, so an application on three instances sends three digests. As of writing, version 4 adds a \`distributed: true\` option that lets a single instance execute each fire, and a \`noOverlap\` option that skips a run while the previous one is still going. For work that has to run on schedule regardless of how many instances the application has, a scheduler outside the app process is the more predictable choice.

### What is the difference between cron and crontab?

\`cron\` is the daemon: the background service on a Unix-like system that checks every minute which jobs are due and starts them. A crontab is the table it reads, a file of schedule lines with one job per line, kept per user or system-wide and usually edited with \`crontab -e\`. The distinction becomes practical once an application runs in containers. A crontab belongs to one machine, so a crontab baked into an application image is copied into every container started from that image, and if each container runs a cron daemon, each one runs every job. That is the container version of the multiple-instances problem, and the fix is the same as for an in-process scheduler: run the schedule in one place outside the replicated service, or make every job refuse to run twice for the same scheduled time.

### How can I enable a cron job on only one instance?

There are three common approaches. The first is a distributed lock: every instance keeps the schedule, and at fire time they race for a lock in a shared store such as Redis or the database, so one runs and the rest skip. ShedLock for Spring, Laravel's \`onOneServer()\` and the \`distributed: true\` option in \`node-cron\` version 4 work this way. The second is a unique record per run, such as a row keyed on task name and scheduled time, so a second attempt fails at insert; Rails' Solid Queue uses this for recurring tasks. The third is to take the schedule out of the replicated service entirely and run it as a single scheduler process or as a platform cron job that starts its own container. Locks are the quickest to add, but they skip rather than retry if the instance holding the lock crashes. Whichever you pick, make the job itself idempotent, because even Kubernetes documents its CronJob scheduling as approximately once.

### Is setInterval reliable for scheduled tasks?

For short, frequent housekeeping inside a process, it is fine. For anything that has to happen at a time of day, it is not. \`setInterval\` counts from when the process started rather than from the clock, so a 24-hour interval fires 24 hours after the last restart and shifts with every deploy; an application deployed more often than once a day never reaches the first tick. It also runs only while the process is alive, so a crash, a scale-down or a hosting plan that puts idle containers to sleep stops it without any error. And like any in-process timer it runs once per instance, so three replicas run the task three times. A cron-expression scheduler fixes the drift but not the other two problems. For daily or weekly work, a scheduler outside the application combined with a job that checks it has not already run for that scheduled time is the dependable setup.

Keep reading

---

# 10 European Netlify Alternatives for Static Sites (2026)

> European Netlify alternatives for static sites: who operates each, where the data sits, and what happens when the free tier runs out.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. 10 European Netlify Alternatives for Static Sites (2026)

DeploymentSeptember 26, 202612 min read

# 10 European Netlify Alternatives for Static Sites (2026)

Ten places to host a static site in Europe instead of Netlify, compared by who operates each one, what the free tier covers, and what happens on the day it runs out.

## The short version

The European static hosts closest to Netlify are statichost.eu, Codeberg Pages and Runsite Static Sites: all three are free and deploy from Git. Established French operators such as Clever Cloud and Scalingo offer trials or credits instead, and the real differences are who operates each service and what happens to your site when the free allowance runs out.

[Teo Marquardt](https://runsite.app/about#author)· Facts last checked September 26, 2026

The short answer first. If you want a free static host run from Europe that builds on every push, statichost.eu and Codeberg Pages are the closest thing to Netlify's core loop, and Runsite Static Sites adds a preview URL on every pull request without billing the site at all.

If you want an established company behind it, Clever Cloud and Scaleway are both French and both serve static output, though neither has a free tier. And if you'd rather have no vendor in the middle, Coolify on a European VPS gives you the same build-and-serve loop on a machine you rent.

People tend to arrive at this question for one of two reasons. The first is the free plan. Netlify moved to credit-based pricing, and on the free plan the credits have a hard limit: when they are gone, the sites stop serving until the next cycle. The second is jurisdiction. Netlify, Inc. is a San Francisco company, and at some point somebody in legal, or a customer's security questionnaire, asks who operates the thing that serves your pages.

This page is about sites that compile to files: Astro, Hugo, Eleventy, a Vite build, a Next.js static export. If your app needs a server process, a database or scheduled jobs, you are shopping for a different kind of platform, and [the full-stack comparison of Vercel alternatives](https://runsite.app/blog/vercel-alternatives) covers that one.

## 10 European Netlify alternatives at a glance

Two columns in this table are missing from most lists. One is the company that operates the service and where it is registered, since that is what a data-protection question is really about. The other is what happens when the free part runs out, which varies far more than the words "free tier" suggest. Prices are left out on purpose. They change faster than articles get updated, and a wrong price is worse than none.

| Platform                  | Operator registered in                             | Free for static sites?                        | Deploy from Git?                                   | When the free part runs out                                               |
| ------------------------- | -------------------------------------------------- | --------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------- |
| statichost.eu             | Sweden (single operator, service in beta)          | Yes: 1 site, 10 GB traffic, 100 build minutes | Yes: GitHub, GitLab, Bitbucket, Forgejo, SourceHut | Move to a paid plan                                                       |
| Codeberg Pages            | Germany (Codeberg e.V., non-profit)                | Yes, for repositories hosted on Codeberg      | Codeberg only                                      | No paid tier; it is a community service                                   |
| IONOS Deploy Now          | Germany (IONOS SE)                                 | Yes: 3 starter projects, 50 MB per deployment | GitHub only                                        | Move to a paid plan                                                       |
| Clever Cloud              | France                                             | No, sign-up credits                           | Yes: git push or CLI                               | Per-second billing                                                        |
| Scaleway                  | France                                             | No; 75 GB of egress a month included          | No: upload or S3 CLI                               | Billed for storage and traffic                                            |
| Bunny.net                 | Slovenia                                           | No, 14-day trial                              | No: FTP, SFTP or API                               | Usage billing with a monthly minimum                                      |
| Scalingo                  | France                                             | No, trial with a card on file                 | Yes: git push                                      | Billing starts when the trial ends                                        |
| Upsun                     | France                                             | No, 15-day trial                              | Yes: git push or CLI                               | Move to a paid plan                                                       |
| Coolify on a European VPS | Nobody: open source, you run it                    | The software is free, the server is not       | Yes: GitHub, GitLab, Bitbucket, Gitea              | You keep paying for the VPS                                               |
| Runsite Static Sites      | Not incorporated: sole operator, EU infrastructure | Yes: not billed, 100 GB traffic included      | Yes, with a preview URL on every pull request      | Nothing today; usage pricing above the quota is announced (as of writing) |

European Netlify alternatives for static sites, checked against each vendor's own pages in September 2026.

## The 10 alternatives, one by one

### 1\. statichost.eu: the closest thing to Netlify, built in Sweden

statichost does the part of Netlify most people actually use. You connect a repository, it builds on every push, and it serves the output on your own domain with HTTPS set up for you. The list of supported Git hosts is wider than Netlify's, with Forgejo and SourceHut next to GitHub, GitLab and Bitbucket, which matters if you have already moved your code off GitHub. The [free plan](https://www.statichost.eu/pricing/) covers one site, 10 GB of traffic and 100 build minutes a month as of writing.

Read the terms before you put a client's site on it. The operator is a single registered business in Stockholm, the terms describe the service as a beta run on a best-effort basis, and branch and pull-request previews are listed as coming soon. For a personal site, a portfolio or project docs, none of that is a problem. For a site with an SLA behind it, it might be.

### 2\. Codeberg Pages: free hosting for sites that live in open-source repos

Codeberg e.V. is a Berlin non-profit that runs a Forgejo-based code forge, and [Codeberg Pages](https://docs.codeberg.org/codeberg-pages/) serves a site straight from a repository: either the files on a `pages` branch or the output of a Forgejo Actions build for a static site generator. Custom domains work through a CNAME and a TXT record. There is no bill because there is no paid tier.

The catch is that it only works for code hosted on Codeberg, so choosing it usually means leaving GitHub as well, and Codeberg is aimed at free and open-source projects. The service is also mid-migration as of writing: the older Pages server no longer accepts new users, and new sites go through the newer git-pages setup. It is the best answer on this list for an open-source project's documentation and a poor one for a company's marketing site.

### 3\. IONOS Deploy Now: a German operator with a GitHub-only workflow

IONOS SE, based in Montabaur, is one of Germany's largest hosting companies, and Deploy Now is its answer to Netlify. It connects to a GitHub repository through a GitHub App, builds on push, gives feature branches their own preview URLs and serves the result on a custom domain. The free starter plan allows three projects and 50 MB per deployment, which is enough for most documentation sites and small marketing pages.

If you picked it for the German address, look at where the data goes. IONOS states that Deploy Now data is mirrored across data centres in the US, the UK and Germany, so a German operator does not mean German-only storage here. And as of writing, the international ionos.com storefront names IONOS Inc. in the US as its operator, so check which entity is on your contract. GitLab and other forges are not supported.

### 4\. Clever Cloud: a static runtime inside a full French PaaS

Clever Cloud SAS, based in Nantes, runs a general-purpose platform, and static sites are one runtime among many. `clever create --type static` sets one up, and the [static runtime](https://www.clever.cloud/developers/doc/deploy/applications/static/) detects and builds Astro, Docusaurus, Hugo, mdBook, MkDocs, Nuxt, VitePress, Zola and several other generators without extra configuration. You deploy with git push or the Clever Tools CLI.

There is no free tier, only credits at sign-up, then per-second billing. For a site on its own that is more platform than you need. It makes sense when the static front end sits next to an API and a database that you also want run by a French company, so the whole product has one operator and one contract.

### 5\. Scaleway: a storage bucket that serves a website

Scaleway SAS in Paris offers website hosting from an Object Storage bucket, and Edge Services puts a custom domain with a managed Let's Encrypt certificate in front of it. Buckets live in Paris, Amsterdam or Warsaw, and the storage pricing includes 75 GB of egress a month as of writing.

This is file hosting rather than a Netlify-style platform. Nothing builds your site: you run the build in CI and upload the output with the S3 CLI, and there are no preview deploys unless you script them yourself. If you already have a pipeline and want the files held by a French provider at a per-gigabyte price, it is a clean option. If you want push-to-deploy, it isn't.

### 6\. Bunny.net: storage and a CDN, billed by the gigabyte

BunnyWay d.o.o. in Ljubljana sells a CDN with storage attached. A storage zone holds your files, a pull zone serves them, and you choose the storage region, with EU options including Frankfurt, Stockholm, Madrid and Prague. Uploads go through FTP, SFTP or the HTTP API. There is no Git integration and no build step.

After a 14-day trial it is usage billing with a small monthly minimum. People who pick Bunny usually want control over caching and a bill measured in gigabytes rather than seats or credits, and they are happy to run the build themselves. Like Scaleway, it replaces Netlify's CDN rather than Netlify's workflow.

### 7\. Scalingo: git push, an nginx buildpack, and a compliance paper trail

Scalingo SAS in Strasbourg is a Heroku-style platform: you push, a buildpack builds, a container serves. Static sites go through the nginx buildpack, with custom domains and TLS documented. The free trial asks for a card and turns into billing automatically when it ends, so it is not a free home for a hobby site.

Where Scalingo earns its place is procurement. If the product your static site belongs to already runs on Scalingo, or has to pass a review that asks for a French operator with French infrastructure, putting the marketing site on the same account keeps the paperwork to one vendor.

### 8\. Upsun: static sites as one app in a larger project

Upsun, formerly Platform.sh, is operated by Platform.sh SAS in Paris. A static site is one application in a project, configured to serve a directory, deployed with git push or the CLI, and Upsun's model is built around separate environments per branch. There is a 15-day trial with no card, and no free tier after it.

The detail to check is the region. Upsun runs on AWS, Google Cloud, Azure, IBM Cloud and OVHcloud, so the operator is French but the hardware can belong to a US hyperscaler. If the question you are answering is who holds the servers, pick an OVHcloud region deliberately.

### 9\. Coolify on a European VPS: no vendor between you and the server

Coolify is open-source software you install on a server you rent, from Hetzner, Scaleway or OVHcloud if you want it in Europe. Its [static build pack](https://coolify.io/docs/applications/build-packs/static) serves a repository's files with Nginx, and its Nixpacks builder can run a static site generator first. It connects to GitHub, GitLab, Bitbucket and Gitea, and it does preview deployments.

The software costs nothing and the server is cheap. What you take on is the operator's job: updates, backups, TLS renewals that fail at 2 a.m., and the day the disk fills up. For one static site that is a lot of machinery. For a team already running other apps on the same box it is almost free, and [what self-hosting a PaaS actually costs](https://runsite.app/blog/coolify-vs-managed-paas) puts numbers on the hours.

### 10\. Runsite Static Sites: free static hosting with PR previews, from Germany

Ours, with the parts that may not suit you. You connect a repository, every push to the production branch builds and deploys, and every pull request gets its own preview URL. Small sites usually build in about twenty seconds. Custom domains come with free SSL. Static sites are not billed: there is no plan and no hourly charge, they never draw on your balance or the starting credit, and 100 GB of traffic a month is included.

Now the limits. There is no form handling and there are no serverless functions, both of which Netlify has. Builds and origin storage are in Germany, on Hetzner, but DNS, TLS and the edge cache for public assets run through Cloudflare, Inc., a US company, as listed on our [compliance page](https://runsite.app/compliance). Runsite has no incorporated company behind it yet. And usage pricing for traffic above the included quota is announced for after launch, with notice before anything changes.

The US names on every other list

Cloudflare Pages, GitHub Pages and Vercel appear in almost every list of Netlify alternatives, and they are good products. They are also operated by US companies, Cloudflare, Inc. and GitHub, Inc. in San Francisco and Vercel Inc., which is the same jurisdiction as Netlify, Inc. Moving between them changes the pricing model and the dashboard. It doesn't change the answer to who operates the service.

## Is there a free alternative to Netlify?

Yes, several, and "free" means something different on each. Four of the ten host a static site at no cost: statichost.eu for one site within its traffic and build limits, Codeberg Pages for projects hosted on Codeberg, IONOS Deploy Now for up to three small starter projects, and Runsite Static Sites, which doesn't bill static sites and includes 100 GB of traffic.

The rest offer something that sounds similar but isn't. Clever Cloud gives credits at sign-up, Scalingo and Upsun give time-limited trials, Bunny.net a 14-day trial, and Scaleway includes an egress allowance but charges for storage. Coolify is free software on a server you pay for. None of those is a place to leave a hobby site for three years and forget about it.

Whichever one you pick, the question worth asking about any free tier is what happens on the day you go over it. Netlify's answer is the reason a lot of people started searching.

## When the free credits run out: "Site not available"

Netlify's free plan is credit-based as of writing. Its documentation describes it as "300 credits/month with credit hard limit" and adds, for the free plan, "Hard limit so no recharge option". The hard limit is a real protection: nobody on the free plan wakes up to a four-figure bandwidth bill, which is the fear the top Reddit thread on Netlify alternatives opens with. What the limit does instead is spelled out in Netlify's [billing FAQ](https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/billing-faq-for-credit-based-plans/):

From Netlify's billing FAQ

"Once your monthly credit allotment is used up for all web projects on your team, all of your web projects (sites/apps) are paused and visitors to your web projects will find a `Site not available` page at each of your web project's URLs."

The sentence is easy to skim past. The pause applies to every project on the team, not only the one that used the credits, so a traffic spike on one site takes the others down with it. And while the projects are paused, the FAQ says they receive no web traffic or form submissions and you cannot trigger new production deploys. On the free plan, the way out is to wait for the next billing cycle or upgrade.

That trade isn't wrong, but it is worth choosing on purpose. A cap that takes the site offline protects your wallet and costs you the day the site was busy, which for a launch page is the only day that mattered. The other models on this list are a bill that keeps growing, a plan limit that asks you to upgrade, or, for Runsite as of writing, traffic that isn't billed at all within the quota. Why serving files costs anyone money in the first place is covered in [the arithmetic of egress fees](https://runsite.app/blog/s3-egress-fees).

## Often listed, but not a Netlify replacement

Three names turn up in European lists that don't hold up once you check what they actually offer for a static site.

* Hetzner Object Storage. Hetzner is the default cheap European provider, so it gets listed everywhere. But its [documentation](https://docs.hetzner.com/storage/object-storage/supported-actions/) lists website hosting among unsupported actions, and custom domains for buckets are not supported either. To serve a site from Hetzner you need a server in front of the storage, which is the Coolify entry above.
* Infomaniak. Infomaniak Network SA is in Geneva, outside the EU; Switzerland has an EU adequacy decision as of writing. It sells web hosting with Git support, and its free Starter plan is 10 MB of plain HTML that comes with a domain registered there. That is enough for one page, not for a build pipeline.
* Bopbee. An EU-hosted drag-and-drop static host with a free plan of one project and 3 MB, kept alive by visiting the dashboard once a month. Its terms name French law and Paris courts, but not the company that runs it, and the operator is the one thing a data-residency question needs answered.

## Which one, by situation

### Free and in Europe

For a single personal site with deploys from Git, statichost.eu is the most direct swap. If you have more than one site or want a preview URL on every pull request, Runsite Static Sites does both without billing them. If the code is open source and you're willing to host it on Codeberg, Codeberg Pages costs nothing and has no company behind it at all.

### Open source, no vendor at all

Codeberg Pages if a community-run service is acceptable, Coolify on your own European VPS if it has to be your machine. The first asks nothing of you but your repository's location. The second asks for your weekends.

### You need forms and functions

This is the part of Netlify that none of the static hosts here copy. Form handling and serverless functions come bundled with Netlify and are absent from statichost, Codeberg Pages, IONOS Deploy Now and Runsite Static Sites. The European answer is to run a small backend next to the static site on a platform that runs processes, such as Clever Cloud or Upsun, or a web service on the same account as your static site, and to point the form at it.

### Compliance asks who the operator is

Clever Cloud, Scalingo and Scaleway are registered French companies with legal notices you can cite in a questionnaire. IONOS is German but mirrors Deploy Now data to the US and the UK. Upsun is French but may run on a US hyperscaler depending on the region. The different things "European" can mean, and which of them survive a security review, are set out in [choosing a European Heroku alternative](https://runsite.app/blog/european-heroku-alternative).

## How Runsite handles it

A static site on Runsite is a repository and a build command. Every push to the production branch builds and deploys, every pull request gets a preview URL that stays stable while you push to the branch, and the build detects the common generators, including Next.js static export, Astro, Hugo, Vite and SvelteKit. The mechanics of that loop are in [how git push to deploy works](https://runsite.app/blog/git-push-to-deploy). [Static hosting that isn't billed](https://runsite.app/services/static-sites) is the whole offer: no plan, no hourly charge, 100 GB of traffic a month, custom domains with free SSL.

The chain behind it is Hetzner in Germany for builds and origin storage, and Cloudflare for DNS, TLS and the edge cache, both named on the compliance page. If your site needs a form endpoint or an API, a web service on the Nano plan costs €3 a month and the €5 starting credit covers it, on the same account and the same infrastructure.

## The short version

* Free, European and closest to Netlify's workflow: statichost.eu (Sweden, beta), Codeberg Pages (Berlin, open-source projects) and Runsite Static Sites (Germany, PR previews, not billed).
* An established French company: Clever Cloud or Scalingo if you want git push, Scaleway if you're happy to upload build output to a bucket. None of them has a free tier for static sites.
* No vendor: Coolify on a European VPS, if you'll run the server.
* Netlify's free plan pauses every site on the team with a `Site not available` page when its 300 monthly credits run out. Whatever you move to, find out what it does on that day.
* None of the European static hosts here replaces Netlify Forms or Functions. Plan for a small backend if you rely on them.

Related service

## Static Sites on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Static Sites](https://runsite.app/services/static-sites)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is there a free alternative to Netlify?

Yes. For static sites, statichost.eu has a free plan for one site with 10 GB of traffic and 100 build minutes a month, Codeberg Pages is free for projects hosted on Codeberg, IONOS Deploy Now has a free starter plan for up to three small projects, and Runsite Static Sites doesn't bill static sites and includes 100 GB of traffic a month. Cloudflare Pages and GitHub Pages are also free but are operated by US companies. Clever Cloud, Scalingo, Upsun and Bunny.net offer trials or credits rather than a free tier.

### Which is better, Netlify or Vercel?

For a site that compiles to static files, the two are close: both build from Git, give deploy previews and serve from a CDN, and Netlify has built-in form handling that Vercel doesn't. Vercel is the stronger choice for Next.js features that need a server, since the framework is developed by Vercel. Both are US companies. If you need a long-running server, a database or background jobs, neither is the natural fit, and a container platform is the better comparison.

### Is Netlify free for lifetime?

Netlify's free plan has no end date, but it is credit-based as of writing: 300 credits a month with a hard limit and no option to recharge. When the credits are used up, Netlify's billing FAQ says every project on the team is paused and visitors see a "Site not available" page until the next billing cycle or an upgrade. So the plan is free for as long as your usage stays under the credit limit.

### What is the best hosting provider for Europe?

For static sites it depends on what you need. statichost.eu (Sweden) and Runsite Static Sites (infrastructure in Germany) are free and deploy from Git; Codeberg Pages (Berlin, non-profit) suits open-source projects; Clever Cloud and Scalingo are established French platforms with git push but no free tier; Scaleway and Bunny.net suit teams that build in CI and upload files. If a customer asks where your data is, check both the operator's country and where the servers and CDN run, because they are often different.

Keep reading

---

# 9 Render Alternatives in 2026, Compared by Operator

> Nine Render.com alternatives for backends: who operates each, how billing differs, why Frankfurt isn't EU hosting, and what render.yaml won't move.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. 9 Render Alternatives in 2026, and What Your render.yaml Doesn't Carry

DeploymentSeptember 26, 202613 min read

# 9 Render Alternatives in 2026, and What Your render.yaml Doesn't Carry

Nine platforms that replace Render.com, sorted by who operates them and how the bill differs from Render's, plus a line-by-line read of what a render.yaml moves and what it quietly leaves behind.

## The short version

Pick the destination by the problem that sent you looking. Railway and Fly.io meter usage instead of charging for an instance tier, which changes the bill but not the jurisdiction. Coolify on a rented European server shrinks the invoice. Clever Cloud, Scalingo and Upsun answer a customer asking whose law governs their data. Then read your render.yaml line by line, because generated secrets, sync: false values and disk contents never leave Render on their own.

[Teo Marquardt](https://runsite.app/about#author)· Facts last checked September 26, 2026

The short version first. Railway is the closest lateral move if you want usage billing instead of instance tiers. Fly.io suits an app that has to sit near users on several continents. Northflank adds production controls and can run inside a cloud account you own. Coolify and Dokploy on a rented European server cut the invoice furthest and hand you the server. Clever Cloud, Scalingo and Upsun are run by French companies, which is the property that changes the answer when a customer asks whose law applies to their data. Sliplane runs your container in Germany and not much else. Runsite, which publishes this page, runs in Germany, is operated by one person rather than a company, and reads a render.yaml directly.

Most Render alternatives lists are written by one of the platforms on them, and each one comes first in its own ranking. This one is no different in that respect, so the table below sorts on two things that do not depend on who wrote it: where the operating company is registered, and how the bill is shaped compared with Render's. After that comes the part the lists skip, which is what inside your `render.yaml` survives the trip.

## Render alternatives for backend services: nine options in 2026

Render bills in two layers. There is a workspace plan ($0 for Hobby with one seat, $25/mo for Pro, $499/mo for Scale), and then each service runs on a fixed instance type, such as $7/mo for 0.5 CPU and 512 MB, [prorated to the second](https://render.com/pricing). Postgres, Key Value and disks are priced separately, and bandwidth beyond the included 5 GB (Hobby) or 25 GB (Pro) is $0.15 per GB. Every row below is described relative to that.

| Platform            | Where the operator is registered                                                   | How the bill differs from Render                                               | Best for                                                         |
| ------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| Railway             | [United States](https://railway.com/legal/terms) (Railway Corporation)             | Metered RAM, CPU and egress instead of instance tiers                          | Apps that idle most of the day                                   |
| Fly.io              | [United States](https://fly.io/legal/terms-of-service/) (Fly.io, Inc.)             | Per-second Machines that can stop when idle                                    | Latency-bound apps in several regions                            |
| Northflank          | [United Kingdom](https://northflank.com/legal/terms) (Northflank Ltd)              | Per-second usage, optionally in your own cloud account                         | Teams that want pipelines and to keep the infrastructure account |
| Coolify / Dokploy   | [Open source](https://github.com/coollabsio/coolify); you rent the server          | Your server bill and nothing else                                              | The smallest invoice, if you will run the box                    |
| Clever Cloud        | [France](https://clever.cloud/legal-notice/) (Clever Cloud SAS, Nantes)            | Per-second, scales with load                                                   | A broad managed catalogue under an EU operator                   |
| Scalingo            | [France](https://scalingo.com/legal-notice) (Scalingo SAS, Strasbourg)             | Per-minute, fixed container sizes                                              | Workloads that face a security questionnaire                     |
| Upsun (Platform.sh) | [France](https://upsun.com/trust-center/legal/impressum/) (Platform.sh SAS, Paris) | Project fee plus resources plus per-user licences                              | Teams that clone full environments per branch                    |
| Sliplane            | Germany (Berlin)                                                                   | [Per server](https://sliplane.io/pricing), as many containers as fit on it     | One container in Germany, nothing to administer                  |
| Runsite             | [Not incorporated](https://runsite.app/compliance): sole operator, servers in Germany                 | Fixed plan per service, billed hourly from a prepaid balance, no workspace fee | Small teams leaving Render with a render.yaml in the repo        |

Nine alternatives to Render.com. Operator details link to each company's terms or legal notice; billing checked against each pricing page, Render's on 26 September 2026 and the others on 13 September 2026.

### 1\. Railway: usage billing instead of instance tiers

Railway is the move people make when Render's instance tiers feel like paying for capacity that sits idle. It [meters the RAM, CPU, egress and volume storage](https://docs.railway.com/pricing/plans) a service actually uses, so a quiet app costs less than a $7 instance and a busy one costs whatever it used. The workflow is close enough to Render's that the first deploy is usually uneventful. The trade is the reverse of Render's: the invoice stops being a known number. How the two billing models fail, and in which direction, is laid out in [Render vs Railway](https://runsite.app/blog/render-vs-railway). Railway Corporation is based in San Francisco, like Render.

### 2\. Fly.io: when the app has to be near the user

Fly.io [bills Machines per second](https://fly.io/docs/about/pricing/) and its proxy can [stop an idle Machine and start it on the next request](https://fly.io/docs/launch/autostop-autostart/). That looks a lot like Render's free-tier sleep, except it is a setting you choose on a paid plan. Where Fly earns its place is geography: if your users are in Frankfurt, São Paulo and Sydney, it will put a copy of the app in each. If they are all in Europe, that capability costs you complexity and buys nothing. Fly.io, Inc. is a US company.

### 3\. Northflank: production controls, and the option to bring your own cloud

Northflank shows up on Render shortlists because it covers the things Render's Pro plan charges for, like pipelines, preview environments and release controls, and because it can [run inside a cloud account you own](https://northflank.com/docs/v1/application/bring-your-own-cloud/use-other-cloud-providers-with-northflank). The operator is Northflank Ltd in London. The UK is outside the EU and covered by an adequacy decision that [runs until 27 December 2031](https://eur-lex.europa.eu/eli/dec%5Fimpl/2025/2574/oj/eng), which is fine for most teams and a line item for anyone filling in a procurement form.

### 4\. Coolify or Dokploy: the smallest invoice, and you own the server

These are control planes, not hosts. You install one on a server from Hetzner, Scaleway, OVHcloud or UpCloud and get a push-to-deploy dashboard with databases next to it. A backend that runs about $43 a month on Render (a $7 web service, a $7 worker, a $19 Postgres and a $10 Key Value) fits on a server that costs a fraction of that. What you take on in exchange is the operating system, the reverse proxy, certificate renewal, backups and upgrades of the control plane itself. That trade is priced hour by hour in [what self-hosting a PaaS actually costs](https://runsite.app/blog/coolify-vs-managed-paas).

### 5\. Clever Cloud: a French operator with a wide catalogue

Clever Cloud SAS, registered in Nantes, runs application runtimes, Docker, managed databases and object storage, with compute [billed by the second](https://www.clever.cloud/pricing/). It is the closest European match to Render's breadth.

### 6\. Scalingo: when the operator has to survive a compliance review

Scalingo SAS in Strasbourg is built around being easy to put through a security review: a European operator, European infrastructure, and [ISO 27001 and HDS certification](https://doc.scalingo.com/security/overview/compliance). Most products are [billed by the minute](https://doc.scalingo.com/platform/billing/process). If the thing pushing you off Render is a customer's questionnaire rather than the price, this is the row that answers it most directly. What those reviews actually ask is covered in [GDPR data residency](https://runsite.app/blog/gdpr-data-residency).

### 7\. Upsun: an environment per branch, data included

Upsun, from Platform.sh SAS in Paris, treats an environment as something you clone: by default [a new environment inherits its parent's data and services](https://developer.upsun.com/docs/environments), databases included. Render's full-stack previews on the Pro plan are the nearest equivalent, but they start from seed data rather than a copy. Upsun's [billing has four parts](https://developer.upsun.com/docs/administration/pricing) (project fee, resources, per-user licences, add-ons), so model it before committing.

### 8\. Sliplane: your container on European servers

Sliplane is a Berlin company that runs Docker containers on European servers, Germany and Finland among the locations, without asking you to administer the machine. It [bills per server rather than per service](https://sliplane.io/pricing), and one server runs as many containers as fit on it, which is the opposite of Render's one-line-per-service invoice. It has fewer managed services than Render, so a Postgres and a Key Value instance would need a home elsewhere or would run as containers you look after. If your render.yaml is one web service and nothing else, it is a short move.

### 9\. Runsite: reads your render.yaml, runs it in Germany

Ours, with the limits first. Runsite is operated by an individual developer, not a registered company, so if your procurement review needs an EU company registration number, the three French operators above are the answer. There is one region, in Germany. Plans are fixed per service and billed by the hour from a prepaid balance, with no workspace fee on top. The entry points line up closely with Render's: a Starter [web service](https://runsite.app/services/web-services) is 0.5 vCPU and 512 MB for €5/mo, always on, with 1 TB of bandwidth, against Render's $7 instance of the same size with 5 GB included on Hobby. Runsite can also import a render.yaml straight from your repository, which is described further down along with what it leaves for you to do.

The column that decides it

Three of the nine are run by US or UK companies, same as Render. Two are software you run yourself. Three are French companies, one is German, and one (ours) has no company behind it. Moving from Render to another US operator changes your bill and your dashboard and leaves the jurisdiction question where it was, in whichever region you deploy. If nobody has asked you whose law applies to your customers' data, ignore that column. If somebody has, it is the only one that matters.

## Why people leave Render

The reasons show up at different points in a project's life, and the first one usually shows up on day one.

The first is the free tier going to sleep. A free web service spins down after 15 minutes without traffic and [takes about a minute to come back](https://render.com/docs/free), which is fine for a side project and bad for a demo link or [a webhook that GitHub won't resend](https://runsite.app/blog/github-webhook-timed-out). Why that minute exists, and why every free tier has some version of it, is in [cold starts](https://runsite.app/blog/cold-starts).

The second is the bill adding up by the service. Each web service, worker, database and Key Value instance is its own line, and a modest backend with an API, a worker, a Postgres and a cache is four lines before anyone joins the workspace. Once a second person does, the $25/mo Pro workspace joins the invoice too, because Hobby has one seat. None of this is hidden; it just grows faster than people expect when they started on free.

The third arrives later and has nothing to do with money: somebody asks which company processes the data and under which country's law. That is the subject of the next section but one.

## Is Render really free?

Partly, and with clocks attached. Static sites are free to deploy. Free web services get [750 Free instance hours per workspace per month](https://render.com/docs/free); when those run out, every free web service in the workspace is suspended until the next month starts. A free Postgres database expires 30 days after it is created, with a 14-day grace period to upgrade before Render deletes it along with its data. A free Key Value instance does not persist to disk, so a restart empties it. Free services also cannot use persistent disks, SSH or outbound SMTP ports.

So the free tier is real, and it is shaped for trying things rather than keeping them. The database clock is the one to watch, since the thing that expires first is the thing holding your data.

## Render Frankfurt is a region, not a jurisdiction

Render offers [five regions](https://render.com/docs/regions): Oregon, Ohio, Virginia, Frankfurt and Singapore. Picking `frankfurt` puts the containers and the database in Germany, and it is a sensible choice for European users on latency alone. It does not change the company you have a contract with, which is Render Services, Inc. in San Francisco, and a US company's obligations under US law follow the company rather than the rack. Region is also fixed at creation. A service or database cannot be moved to another region later, and services in different regions cannot reach each other over the private network, so a region change is a migration even inside Render.

If the reason you are reading this is a customer asking where their data is, the region answers half the question. The other half is who can be compelled to hand it over, and that part is covered in [EU vs US hosting](https://runsite.app/blog/eu-vs-us-hosting) and, for the legal mechanics, [Schrems II and EU data transfers](https://runsite.app/blog/schrems-ii-data-transfers).

## What's in your render.yaml, and what it doesn't know

If you deployed with a [Blueprint](https://render.com/docs/blueprint-spec), your infrastructure is already written down, and that is a head start most migrations do not get. It is not the whole picture, though. A render.yaml describes the shape of your services. Some of what matters lives only inside Render, and the file just points at it. Here is a typical one for a small backend:

yaml

```
services:
  - type: web
    name: api
    runtime: node
    region: frankfurt
    plan: starter
    buildCommand: npm ci && npm run build
    startCommand: node dist/server.js
    preDeployCommand: npm run migrate
    healthCheckPath: /healthz
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: main-db
          property: connectionString
      - key: REDIS_URL
        fromService:
          type: keyvalue
          name: cache
          property: connectionString
      - key: SESSION_SECRET
        generateValue: true
      - key: STRIPE_SECRET_KEY
        sync: false
    disk:
      name: uploads
      mountPath: /var/data
      sizeGB: 5

  - type: keyvalue
    name: cache
    ipAllowList: []

  - type: worker
    name: jobs
    runtime: node
    startCommand: node dist/worker.js

  - type: cron
    name: nightly-report
    runtime: node
    schedule: "0 3 * * *"
    startCommand: node dist/report.js

databases:
  - name: main-db
    postgresMajorVersion: "16"
```

Some of those lines move as they are. Some only make sense inside Render and have to be pointed at new resources. And a few were never really in the file, just referenced from it.

| Line                                                 | Travels?                                   | What to do before you leave                                                                                                    |
| ---------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| buildCommand, startCommand, healthCheckPath, runtime | Yes                                        | Nothing. These are plain strings every platform has a field for.                                                               |
| preDeployCommand                                     | Yes, if the destination has a release step | Check the new platform runs it once per deploy before traffic shifts, not once per instance.                                   |
| fromDatabase, fromService                            | As references only                         | They resolve inside Render. The destination must create the database and cache first, then wire the new connection strings in. |
| generateValue: true                                  | No. The value lives only in Render         | Copy the current value from the dashboard. A new platform will generate a different one.                                       |
| sync: false                                          | No. The value was never in the file        | Export these from the dashboard and set them by hand.                                                                          |
| disk                                                 | No                                         | The contents are in neither the repo nor the database dump. Copy them out, ideally into object storage.                        |
| plan, region                                         | No, and they shouldn't                     | Plan names mean nothing elsewhere. Map by CPU and RAM, not by name.                                                            |
| databases, keyvalue                                  | The definition, not the data               | The data needs a dump and restore. Plan this step first.                                                                       |

What a render.yaml carries to any destination. This is true whichever platform you move to, including ours.

### The generated secret that logs everyone out

`generateValue: true` deserves its own warning because the failure is quiet. Render generated that value once, and it exists only in Render. Any import on the other side, ours included, will generate a fresh random value, which is correct behaviour and exactly what you do not want for certain keys. If it is a session secret, every user is logged out at the cutover. If it is the key your application uses to encrypt fields in the database, the restored data cannot be decrypted. Before you move, open each service in the Render dashboard, copy every generated value you care about, and set it explicitly on the destination.

### Workers, cron jobs and private services

Render gives background workers, cron jobs and private services their own service types. Not every platform draws the same lines. Some run a worker as a second process in the same service, some want a separate service, and some have a scheduler but no internal-only service type. Before you commit, write down each non-web entry in your render.yaml and find where it lands on the destination. The one to be careful with is the cron job: if it is running on both platforms during the cutover, every scheduled email goes out twice. [Running cron jobs on more than one instance](https://runsite.app/blog/cron-job-multiple-instances) covers how to keep that from happening.

### The data, which no manifest describes

A Blueprint creates an empty Postgres. That is the right design, and it means the database move is a separate job with its own plan: a dump, a restore, a measured maintenance window and a way back if the restore goes wrong. Everything else in the file can be redone in minutes. The data cannot, so schedule backwards from how long the restore takes on a copy of your real database. On Render, [paid Postgres instances include logical backups](https://render.com/pricing), which is the natural place to start rehearsing.

## How Runsite handles it

Runsite can import a render.yaml from a connected GitHub, GitLab or Bitbucket repository. You pick the repository and branch, it reads the file from the root, and it shows you a preview of everything it is about to create before anything is created. You can import into an existing project or let it create one from the Blueprint's project name.

On the Runsite side it creates web services and static sites (Node, Python, Go, Ruby, Elixir, Rust or a Dockerfile), a [managed PostgreSQL](https://runsite.app/services/postgresql) on the same major version for each database, and [managed Redis](https://runsite.app/services/redis) for each Key Value entry. Build, start and pre-deploy commands come across, along with the root directory, health check path, auto-deploy setting, static publish path and Dockerfile path. The `fromDatabase` and `fromService` references get pointed at the new database, Redis and web services, and environment groups are expanded into every service that uses them.

What it leaves for you is listed in the preview, not discovered later. `sync: false` values arrive as empty secrets waiting to be filled in. `generateValue` gets a new random value, so copy the old one first if it matters (see above). Background workers, cron jobs and private services show up as skipped. Disks, plan sizes, preview settings and the `networking` and `permissions` blocks are not imported, and with the newer `projects:` syntax only one environment comes across, `production` if there is one. The data doesn't move at all: the databases start empty, and the dump and restore are yours.

After the import, pick plan sizes yourself; a like-for-like Render Starter is Runsite's Starter at 0.5 vCPU and 512 MB for €5/mo. Every new account gets €5 of credit, enough to run one entry-plan service of each type while you check that the import matched what you expected. Everything runs in Germany, nothing replicates outside the EU, and a signed [data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) comes with every account. The subprocessors are named on the [compliance page](https://runsite.app/compliance).

## The short version

Choose the destination by the problem you actually have. If the complaint is paying for idle instances, Railway and Fly.io bill differently but leave you with another US operator. An invoice that's simply too big points to Coolify or Dokploy on a rented European server. A customer asking whose law governs their data points to Clever Cloud, Scalingo or Upsun, since a Frankfurt region on a US platform doesn't change that answer.

Then read your render.yaml before you trust it. Commands and health checks travel fine, references need re-pointing, and generated secrets, `sync: false` values and disk contents live only inside Render until you copy them out. The database moves last, on a plan you rehearsed. If you want to see how much of that file comes across on its own, importing it into a [web service running in Germany](https://runsite.app/services/web-services) takes a few minutes and shows everything it skipped before it creates anything.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is Render really free?

Partly. Static sites are free to deploy, and free web services get 750 instance hours per workspace per month, after which all free web services in the workspace are suspended until the next month. A free web service spins down after 15 minutes without traffic and takes about a minute to wake. A free Postgres database expires 30 days after creation, with a 14-day grace period before Render deletes it and its data, and a free Key Value instance loses its data on restart because it is not persisted to disk. Free services cannot use persistent disks, SSH or outbound SMTP ports. It is a tier for trying things, and the database clock is the one to watch. All of this is as described in Render's documentation on 26 September 2026.

### Which is better, Vercel or Render?

They are built for different shapes of application. Vercel is a frontend platform: static builds and server-rendered pages run as functions, and there is no long-running process of your own. Render runs long-lived containers, background workers, cron jobs and managed Postgres, so it can host a whole backend. If your app is a Next.js frontend talking to someone else's API, Vercel fits. If it needs a process that stays alive, a queue worker, or a database next to it, Render is the closer fit, and so is any of the backend-capable alternatives above. Both are US companies. What a full-stack app needs beyond a frontend platform is covered in the Runsite article on Vercel alternatives.

### How to use Render for free?

Deploy static sites, which are always free, and use free web services for prototypes that can tolerate a one-minute wake after 15 idle minutes. Keep an eye on the 750 free instance hours per workspace per month, since running out suspends every free web service until the month resets. Do not keep data you care about in a free Postgres database: it expires 30 days after creation, and Render deletes it 14 days after that unless you upgrade. Free Key Value is fine as a cache but loses everything on restart. Once something has users, a paid instance is cheaper than the wake time it costs you.

### Is Render better than AWS?

They sit at different layers, so it depends on how much of the stack you want to run. Render is a platform: you push code, it builds, deploys, restarts and patches the runtime, and you get managed Postgres without configuring it. AWS is mostly infrastructure: you get far more control and far more services, and you take on networking, IAM, patching and the deploy pipeline yourself or pay someone to. For a small team shipping a web app, a platform removes most of the operations work. The trade-off, including who is responsible for what under each model, is laid out in the Runsite article on PaaS vs IaaS. For European teams there is a second axis: both Render and AWS are US companies, whichever region you choose.

Keep reading

---

# Migrate From Heroku Postgres: Dump, Cutover, Rollback

> Three ways to move a Heroku Postgres database, how to size the downtime, a cutover that doesn't lose writes, and the restore errors Heroku dumps throw.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Migrating Off Heroku Postgres: The Dump, the Cutover and the Rollback

DeploymentSeptember 26, 202612 min read

# Migrating Off Heroku Postgres: The Dump, the Cutover and the Rollback

Three ways to get a database out of Heroku Postgres, how to work out the downtime before you commit to it, a cutover order that doesn't lose writes, and the point after which rolling back stops being free.

## The short version

Under 20 GB, use heroku pg:backups:capture and download, then pg\_restore with --no-owner and --no-acl. Above that, run pg\_dump yourself. Maintenance mode alone does not stop writes, because worker dynos and Scheduler keep running, so scale them to zero first. Rollback is free until the first write lands in the new database.

[Teo Marquardt](https://runsite.app/about#author)· Facts last checked September 26, 2026

Moving an app off Heroku is mostly configuration. The code goes wherever a container runs, the environment variables get copied across, the Procfile turns into settings. All of that can be done twice if the first attempt goes wrong. The database can't. It's the one part of the move where a mistake costs data rather than an afternoon, and it's the part the platform migration guides spend the least time on.

This page is only about that part: getting the data out of Heroku Postgres, putting it somewhere else, and switching over without losing the writes that arrive in between. The rest of the move (the variables Heroku set for you, the Procfile, the build) is covered in [choosing a European Heroku alternative](https://runsite.app/blog/european-heroku-alternative), along with where to go. The commands below are Heroku's own and standard PostgreSQL tools, so they work whatever the destination is.

## Is Heroku still a thing?

Yes, and nobody is being forced off it. On 6 February 2026 Salesforce [stopped selling Heroku Enterprise contracts to new customers](https://siliconangle.com/2026/02/06/salesforce-stop-selling-enterprise-heroku-subscriptions-scale-back-upgrades/) and moved the platform into what it calls sustaining engineering: security, stability and support continue, new features don't. Existing customers can keep using it and renewing at the same prices, and there is no end-of-life date.

So there's no deadline. What changed is that staying is now a decision about a platform that will look the same in three years as it does today. For some teams that's fine. For others it's the push to do a migration they'd been putting off, and those teams have time to do it properly, which mostly means rehearsing the database step.

## Three ways to get your data out of Heroku Postgres

Which one fits depends on two numbers: the size of the database and how long the app can stop accepting writes. Everything else follows from those.

| Method                                              | Size it suits                        | Downtime                                     | What to watch                                                           |
| --------------------------------------------------- | ------------------------------------ | -------------------------------------------- | ----------------------------------------------------------------------- |
| heroku pg:backups:capture \+ download + pg\_restore | Up to about 20 GB                    | Dump, transfer and restore time              | Heroku says it can time out on busy or larger databases                 |
| pg\_dump directly against DATABASE\_URL             | Any size                             | Same, but you control format and parallelism | Run it from a machine close to the database, not over a home connection |
| Trigger-based replication (e.g. Bucardo)            | Large, or low tolerance for downtime | Minutes at the switch                        | Heroku doesn't offer native logical replication out; more moving parts  |

Methods for moving a Heroku Postgres database, per Heroku's Dev Center and help articles as of 26 September 2026.

### 1\. heroku pg:backups:capture, then download

This is the path Heroku [documents for exports](https://devcenter.heroku.com/articles/heroku-postgres-import-export). PGBackups takes a logical backup in `pg_dump`'s custom format, which is compressed and restores in parallel. Heroku's own guidance is to [use it for moderately loaded databases up to 20 GB](https://devcenter.heroku.com/articles/heroku-postgres-backups); beyond that, or with many schemas or large objects, the capture can time out.

bash

```
# Take a fresh backup and download it
heroku pg:backups:capture --app your-app
heroku pg:backups:download --app your-app   # writes latest.dump

# Or get a signed URL and pull it from a server near the destination
# (the URL expires after 60 minutes)
heroku pg:backups:url --app your-app
```

The `pg:backups:url` variant is the one to use if the destination database is far from your laptop. Download the file on a machine in the same region as the new database, and the transfer stops being the slowest step.

### 2\. pg\_dump directly

For anything past the 20 GB guidance, or when you want control over the format, run `pg_dump` yourself against the connection string. Directory format with `--jobs` dumps several tables at once, and `pg_restore` can read it back in parallel too. Use a `pg_dump` at least as new as the server version, and read `DATABASE_URL` at the moment you run it rather than copying it into a script: Heroku rotates Postgres credentials, so a saved one can go stale.

bash

```
# Parallel dump in directory format
pg_dump "$(heroku config:get DATABASE_URL --app your-app)" \
  --format=directory --jobs=4 --no-owner --no-acl \
  --file=heroku_dump/
```

### 3\. Replicate, then switch

If the database is large and the app can only stop for a few minutes, dump-and-restore won't fit in the window. Heroku's help center says it [doesn't support logical replication with external instances](https://help.heroku.com/TVS8OHTR/does-heroku-postgres-support-logical-replication) on Heroku Postgres, so the native Postgres route isn't available. Teams that needed near-zero downtime have used trigger-based replication instead. Getsafe [wrote up a move with Bucardo](https://medium.com/hellogetsafe/pulling-off-zero-downtime-postgresql-migrations-with-bucardo-and-terraform-1527cca5f989). It works, and it adds triggers to every replicated table on the production database, so rehearse it on a copy first. Most small and mid-sized apps don't need it: a measured maintenance window of half an hour is cheaper than a replication setup.

## How long the downtime will actually be

The honest answer is that nobody can tell you without a rehearsal, and the rehearsal is cheap. Take a backup now, restore it into the destination, and time each step. The downtime on the day is roughly the sum of those times plus the checks you run before reopening.

* Capture or dump time. Grows with data size and with load on the database while it runs.
* Transfer time. Near zero if you download on a server close to the destination, and the slowest step if you don't.
* Restore time. Usually longer than the dump, because `pg_restore` rebuilds every index and re-checks every constraint after loading the rows. `--jobs` helps a lot here.
* Verification. Row counts on your main tables, a login, a write, a read. Budget ten minutes and write the checks down in advance.

Rehearse on a copy that's about the same size as production, not on a seed database. Restore time doesn't scale neatly with size, because a few big indexes can dominate it, and those only show up with real data. Add a margin to whatever you measured, and put that number in the maintenance notice.

## The restore errors a Heroku dump will throw

Heroku dumps carry a few things that only exist on Heroku, and the restore will complain about them. Two of them look alarming and aren't.

### ERROR: schema "\_heroku" does not exist

Backups from Essential-tier databases include event triggers that call functions in Heroku's internal `_heroku` schema. Restoring elsewhere produces `pg_restore: error: could not execute query: ERROR: schema "_heroku" does not exist`, repeated for each trigger. Heroku [says these are internal and don't affect your data](https://help.heroku.com/HYME3MD5/unexpected-statements-in-the-backup-from-a-essential-x-database-is-causing-local-pg%5Frestore-to-fail), and recommends filtering them out with a restore list:

bash

```
pg_restore -l latest.dump > unfiltered.list
sed -e '/EVENT TRIGGER/ s/./;&/' unfiltered.list > filtered.list
pg_restore --use-list=filtered.list --no-owner --no-acl \
  --jobs=4 -d "$NEW_DATABASE_URL" latest.dump
```

### schema "heroku\_ext" does not exist

Heroku installs extensions such as `pg_stat_statements` or `pgcrypto` into a `heroku_ext` schema, and the dump creates them there. On a database that has no such schema, the extension statements fail. The simplest fix is to create the schema on the destination before restoring (`CREATE SCHEMA IF NOT EXISTS heroku_ext;`). Then check with `\dx` that every extension your app uses exists on the new side, in a version it supports. If your schema qualifies functions with `heroku_ext.`, the schema has to stay; if nothing references it, you can move the extensions to `public` after the cutover.

### Ownership and grants

Heroku's database user names are generated, and the destination's won't match. That's what `--no-owner` and `--no-acl` are for: every object ends up owned by the user you restore as, and none of Heroku's grants come along. If your app relies on a separate read-only role, recreate it on the destination and grant it explicitly.

## The cutover, in order

The one thing to know before writing the runbook: [maintenance mode doesn't stop writes](https://devcenter.heroku.com/articles/maintenance-mode). It blocks incoming HTTP requests to web dynos and serves a static page. Heroku's docs are explicit that worker dynos keep running and that Scheduler jobs can still run. A queue worker draining jobs during your final dump writes rows the dump doesn't have, and those rows are lost at the switch. Stop them yourself.

bash

```
heroku maintenance:on --app your-app
# web dynos stop receiving requests; workers and Scheduler do not stop
heroku ps:scale worker=0 --app your-app
```

1. A day or more before: lower the TTL on any DNS record pointing at Heroku, so the switch propagates in minutes. Deploy the app on the new platform against a restored copy and make sure it boots.
2. Put up the notice and run `heroku maintenance:on`.
3. Stop everything else that writes: `heroku ps:scale worker=0` for each non-web process type, and disable the Heroku Scheduler jobs.
4. Take the final backup with `heroku pg:backups:capture` (or run the final `pg_dump`).
5. Restore into the new database with the filtered list, `--no-owner`, `--no-acl` and `--jobs`.
6. Run the checks you wrote down: row counts on key tables, sequences, extensions, a login and a write.
7. Point the new app's `DATABASE_URL` at the new database, start its workers and scheduler, switch DNS.
8. Leave the Heroku app in maintenance mode, with its database untouched, until the rollback window closes.

Take a backup on the new side straight away

Once the restore finishes and the checks pass, take an on-demand backup of the new database before opening traffic. If something goes wrong in the first hour, you want a known-good snapshot that already lives on the destination, not a file on someone's laptop.

### What happens to heroku run rails db:migrate

On Heroku, schema migrations usually run from the Procfile's release phase (`release: rails db:migrate`, `release: python manage.py migrate` and so on) or by hand with `heroku run`. After the move, that line needs a home on the new platform, usually a pre-deploy or release command that runs once per deploy before the new version takes traffic. Don't run it during the cutover itself. Ship the database as it is, confirm the app works on it, and let the next normal deploy run the next migration. Why migrations in a rolling release have to work with both the old and the new code is covered in [how git push to deploy works](https://runsite.app/blog/git-push-to-deploy).

## The rollback you want to have written down

Until the first real write lands in the new database, rolling back costs nothing: point DNS back at Heroku, scale the workers up, run `heroku maintenance:off`. The Heroku database is exactly as you left it, because nothing wrote to it.

After the first write, that stops being true. Every order, signup or message created on the new side exists only there, and rolling back now means dumping those rows and bringing them back to Heroku, which is a second migration in the opposite direction. So decide in advance how long you'll watch before calling it done (an hour of normal traffic is a reasonable default), what would make you roll back in that time, and who makes the call. Keep the Heroku database for a while after that, downgraded if you want to save money, but don't delete it the same week.

## How Runsite handles it

If the destination is Runsite, the database side is ordinary PostgreSQL and needs nothing special. Every [managed PostgreSQL](https://runsite.app/services/postgresql) database has an external connection URL with TLS, so `pg_restore` runs against it from your laptop or a server like it would against any other Postgres. It's backed up automatically every day, and the Backups tab has a Backup now button for the snapshot the callout above recommends. The entry Nano plan is €3/mo (0.1 vCPU, 256 MB, 1 GB of storage), and larger plans go up to 8 vCPU and 16 GB on Business.

What it doesn't have: a Heroku importer, and point-in-time recovery. A restore on Runsite rolls the whole database back to a snapshot, so keep the Heroku database as your fallback during the rollback window. Everything runs in Germany, nothing replicates outside the EU, and a signed [data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) comes with every account. How to connect a service to the database is in the [Runsite docs](https://docs.runsite.app).

## The short version

Heroku isn't going away on a date, so there's time to do this properly. Under 20 GB, `heroku pg:backups:capture` and `pg:backups:download`, then `pg_restore --no-owner --no-acl --jobs`. Above that, `pg_dump` in directory format. For a very large database with almost no tolerance for downtime, trigger-based replication, because Heroku doesn't offer logical replication out.

Rehearse on a full-size copy and time it. Expect the `_heroku` and `heroku_ext` errors and handle them before the day. On the day, maintenance mode isn't enough: scale workers to zero and turn off Scheduler before the final dump. And write down when rollback stops being free, which is the moment the first real write reaches the new database.

Related service

## PostgreSQL on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore PostgreSQL](https://runsite.app/services/postgresql)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is Heroku still a thing?

Yes. Heroku is still running and still sells its standard plans. On 6 February 2026 Salesforce stopped selling Heroku Enterprise contracts to new customers and moved the platform into sustaining engineering, which means security, stability and support continue but no new features are being developed. Existing customers can keep using it and renew at the same prices, and as of 26 September 2026 no end-of-life date has been announced.

### What happened with Heroku?

Two things, four years apart. In November 2022 Heroku removed its free dynos, free Postgres and free Redis, which pushed hobby projects to other platforms. In February 2026 Salesforce, which owns Heroku, announced it would stop developing new features and stop selling Enterprise contracts to new customers, keeping the platform running in sustaining mode. According to SiliconANGLE's report of the announcement, Salesforce is redirecting product and engineering investment toward areas including AI. Existing apps are unaffected in the short term, but teams planning for the next few years are treating it as a signal to schedule a migration.

### Why is Heroku no longer free?

Heroku ended its free tier on 28 November 2022, citing the cost of running it and abuse of free resources. Free dynos, free Heroku Postgres and free Heroku Data for Redis plans were removed. The cheapest options since then are Eco dynos at $5/mo for a shared pool of hours, Basic dynos at $7/mo, Heroku Postgres Essential-0 at $5/mo and the Mini Key-Value Store at $3/mo, as listed on Heroku's pricing page on 26 September 2026.

### What is the best way to migrate a database?

Rehearse it before you do it for real. Take a backup of the production database, restore it into the destination, and time each step, because the restore is usually slower than the dump and the only way to know the downtime is to measure it on data of the real size. On the day, stop every process that writes (web traffic, background workers and scheduled jobs), take the final dump, restore it, verify row counts and a test write, then switch the application over. Keep the old database untouched until you're sure you won't roll back, since rollback is free only until the first write lands on the new side. For Postgres specifically, restore with --no-owner and --no-acl so user names and grants from the old host don't carry over.

Keep reading

---

# PaaS Pricing Comparison 2026: One App, Seven Platforms

> One web app with Postgres, Redis and 100 GB of traffic, priced on Render, Heroku, Railway, Fly.io, DigitalOcean and Vercel, and where each bill grows.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. PaaS Pricing Comparison 2026: One App, Priced on Seven Platforms

DeploymentSeptember 26, 202612 min read

# PaaS Pricing Comparison 2026: One App, Priced on Seven Platforms

The same small production app (a web service, Postgres, Redis and 100 GB of traffic) priced on Render, Heroku, Railway, Fly.io, DigitalOcean, Vercel and Runsite, for one developer and for a team of three.

## The short version

For one small production app with Postgres, Redis and 100 GB of traffic, the monthly bill runs from about $15 on Heroku to about $43 on Fly.io before Redis, as of 26 September 2026\. Adding two teammates changes the order: Render adds a $25 workspace fee and Vercel charges $20 per deploying seat. Bandwidth and the database floor move the total more than the web service does.

[Teo Marquardt](https://runsite.app/about#author)· Facts last checked September 26, 2026

Most PaaS pricing comparisons put plan names side by side: Render's Starter next to Heroku's Basic next to Railway's Hobby. That tells you very little, because the plans are different units. One is a container size, one is a dyno tier, one is a minimum spend. What you actually want to know is what your app will cost, and the only way to get that is to pick an app and price it everywhere.

So that is what this page does. Every figure comes from the provider's own pricing page or documentation, checked on 26 September 2026\. Prices for the US platforms are in dollars, as they publish them, and ours are in euros, as we publish them. Nothing is converted, because a converted price is a price nobody actually charges.

## The reference app

Small, real and boring on purpose, because that's what most apps on these platforms look like:

* One web service with about 0.5 vCPU and 512 MB of RAM, running all month. No sleeping.
* One managed Postgres, the smallest paid plan that isn't a dev or trial database.
* One Redis-compatible cache, around 256 MB.
* 100 GB of outbound traffic a month.
* Priced twice: for one developer, and for a team of three who all deploy.

What it deliberately leaves out: background workers, cron jobs, object storage, build minutes and support plans. Each of those is its own line on every platform and would make the table unreadable. If your app has a worker, add another web-service line; it is priced the same way almost everywhere. Comparing a platform against a plain server instead of against another platform is a different question, answered in [VPS vs PaaS cost](https://runsite.app/blog/vps-vs-paas-cost).

## PaaS pricing comparison 2026: the table

| Platform                  | Web service                    | Postgres                               | Redis / Key Value                | 100 GB egress              | 1 developer       | Team of 3               |
| ------------------------- | ------------------------------ | -------------------------------------- | -------------------------------- | -------------------------- | ----------------- | ----------------------- |
| Heroku                    | $7 (Basic, 512 MB)             | $5 (Essential-0)                       | $3 (Mini, 25 MB)                 | No per-GB line             | **$15**           | **$15**                 |
| Railway                   | ≈ $6 (usage)                   | ≈ $3 (usage, unmanaged)                | ≈ $3 (usage)                     | $5                         | **≈ $17**         | **$20** (Pro minimum)   |
| DigitalOcean App Platform | $5 (1 vCPU, 512 MiB)           | $15.15 (1 GiB)                         | $15 (Valkey, 1 GiB)              | $1 (50 GiB included)       | **$36**           | **$36**                 |
| Render                    | $7 (0.5 CPU, 512 MB)           | $6 (256 MB)                            | $10 (256 MB)                     | $14.25 (5 GB included)     | **$37**           | **$59** (Pro workspace) |
| Fly.io                    | $3.19 (shared, 512 MB)         | $38.28 (Managed Postgres Basic + 1 GB) | Not included (Upstash extension) | $2 (Europe rate)           | **≈ $43 + Redis** | **≈ $43 + Redis**       |
| Vercel                    | No long-running process        | Marketplace partner                    | Marketplace partner              | 1 TB included on Pro       | Not comparable    | $60 in seats alone      |
| Runsite                   | €5 (Starter, 0.5 vCPU, 512 MB) | €3 (Nano, 256 MB)                      | €5 (Starter, 256 MB)             | Included (1 TB on Starter) | **€13**           | **€13**                 |

The reference app priced on each platform's public pricing page, 26 September 2026\. Totals rounded to the nearest dollar or euro. Railway is an estimate because it bills usage; the assumption is stated below. Heroku, DigitalOcean managed databases and Fly Managed Postgres were checked on their pricing and documentation pages the same day. Taxes excluded.

Two things stand out before any caveats. Heroku is the cheapest line in dollars, and the gap between one developer and a team of three is zero on four of the seven platforms and large on the other three. Both are worth unpacking, because neither is what the plan names suggest.

## Render pricing: where the $7 goes

[Render charges for three things](https://render.com/pricing): a workspace plan, metered features like bandwidth, and compute for each service. Compute is a fixed instance type prorated to the second, and the one most people start on is $7/mo for 0.5 CPU and 512 MB. That's the number that shows up in searches for Render pricing, and it's accurate. It just isn't the bill.

The bill adds the database and the cache as separate instances, $6 and $10 at the smallest paid sizes, and then bandwidth. The free Hobby workspace includes 5 GB a month and charges $0.15 per GB after that, so 100 GB of traffic costs $14.25, which is twice the web service. Add a second person and you move to the Pro workspace at $25/mo, which raises the included bandwidth to 25 GB and adds seats, previews and autoscaling. That step is where Render's total jumps from $37 to $59 for the same app.

### Render Postgres pricing

Paid Postgres starts at $6/mo for 0.1 CPU and 256 MB with 1 GB of storage included, then $19/mo for 1 GB of RAM. Extra storage is $0.30 per GB. Point-in-time recovery covers 3 days on Hobby and 7 on Pro. The free Postgres expires after 30 days, which is why it isn't in the table; the details are in [Render alternatives](https://runsite.app/blog/render-alternatives).

## Heroku: cheapest on paper, frozen in place

[Heroku's published prices](https://www.heroku.com/pricing/) are $7/mo for a Basic dyno with 512 MB, $5/mo for Heroku Postgres Essential-0 (1 GB of storage, 20 connections) and $3/mo for the Mini Key-Value Store. Fifteen dollars for the whole reference app, with no per-seat line on the pricing page.

Three caveats before that number wins anything. The Mini cache is 25 MB, a tenth of what the other rows price, and the next size up is $15\. Essential Postgres plans [don't support fork and follow](https://devcenter.heroku.com/articles/heroku-postgres-plans), so there are no replicas, and they come with a 99.5% expected uptime. And the dyno most teams actually run production on is Standard-1X at $25/mo, which is where Heroku's production features start. At Standard-1X with Essential-1 ($9) and Premium-0 Key-Value ($15), the same app is $49.

Then there is the question that has nothing to do with price. Salesforce moved Heroku into [sustaining engineering in February 2026](https://siliconangle.com/2026/02/06/salesforce-stop-selling-enterprise-heroku-subscriptions-scale-back-upgrades/) and stopped selling new Enterprise contracts. The prices haven't changed and there is no end-of-life date, but a platform that is no longer getting new features is a different purchase from one that is. If you are already on it and planning the move, [migrating off Heroku Postgres](https://runsite.app/blog/migrate-from-heroku) is the part that needs a plan.

### Heroku pricing calculator, done by hand

Heroku bills dynos and add-ons by the second up to the monthly cap, so a dyno that runs all month costs its listed price and one you scale down halfway costs half. Multiply the dyno price by the number of dynos for each process type, add each add-on at its plan price, and that's the bill. The trap is the process count: a Procfile with `web` and `worker` lines is two dynos, and a worker on Basic is another $7.

## Railway: an estimate, not a price

[Railway bills what you use](https://railway.com/pricing), by the second: $0.00000386 per GB of RAM per second and $0.00000772 per vCPU per second, which works out to about $10 per GB of RAM and $20 per vCPU over a 30-day month. Egress is $0.05 per GB. The Hobby plan is a $5 minimum that includes $5 of usage, and Pro is a $20 minimum that includes $20 of usage and unlimited seats. Hobby is a single-developer workspace, which is why the team column jumps to Pro.

The ≈ $17 in the table assumes the three services hold about 1 GB of RAM between them all month and average a tenth of a vCPU of real CPU work, plus 100 GB of egress. A busier app costs more and a quieter one less, which is the whole point of usage billing and also why nobody can quote you a Railway price in advance. One more thing the number hides: Railway's Postgres and Redis are [deployed from templates and are unmanaged](https://docs.railway.com/databases), so backups and recovery are yours, and that makes the database line hard to compare with a managed one. How that billing shape fails, and in which direction, is covered in [Render vs Railway](https://runsite.app/blog/render-vs-railway).

## Fly io vs Render vs Railway

These three get compared together more than any others, and on this app they come out in an unexpected order. Fly.io has the cheapest compute in the table, [$3.19/mo for a shared-CPU Machine with 512 MB](https://fly.io/docs/about/pricing/), and European egress at $0.02 per GB. It also has the most expensive database: Managed Postgres starts at $38/mo for the Basic plan, plus $0.28 per GB of storage. Fly doesn't run its own Redis; it offers Upstash as an extension, billed separately. So Fly is the cheapest of the three for an app without a database and the most expensive for one with a managed database.

Render is the predictable one: fixed instances, so the bill is known in advance, with bandwidth as the line to watch. Railway is the cheapest of the three for an app that idles and the hardest to predict for one that doesn't. All three are US companies. None of them offers an answer to the question of whose law governs your data just by choosing a European region, which is covered in [EU vs US hosting](https://runsite.app/blog/eu-vs-us-hosting).

## Vercel: why it isn't in the total

Vercel is in most PaaS pricing comparisons and doesn't fit this one. It runs your code as functions and serves static output from a CDN; there is no process of yours that stays running, and Postgres and Redis come from marketplace partners billed on their own terms. The [Pro plan](https://vercel.com/docs/plans/pro-plan) is a $20/mo platform fee with one deploying seat and $20 of usage credit, then $20/mo for each additional seat that deploys. Viewer seats are free. For a team of three that's $60 a month before the backend is hosted anywhere. What a full-stack app needs beyond a frontend platform is worked through in [Vercel alternatives](https://runsite.app/blog/vercel-alternatives).

## DigitalOcean App Platform pricing

[App Platform](https://www.digitalocean.com/pricing/app-platform) is cheap for the container, $5/mo for a shared vCPU with 512 MiB and 50 GiB of transfer, with extra transfer at $0.02 per GiB. The databases are where the total comes from: [managed PostgreSQL](https://www.digitalocean.com/pricing/managed-databases) starts at $15.15/mo and managed Valkey at $15/mo, each with 1 GiB of RAM, which is more than the other rows price but also the smallest size DigitalOcean sells. There is a $7 development database, and it is explicitly for development. Team members don't cost anything extra.

## Where the bill grows that the pricing page doesn't show

Put the rows side by side and the web service turns out to be the least interesting line on every platform. The totals are driven by four other things, and each platform has a different one.

* Bandwidth. Render's $0.15 per GB beyond the included allowance costs more than the web service at 100 GB, and grows linearly with traffic. Railway charges $0.05, Fly.io $0.02 in Europe. Heroku, DigitalOcean up to the allowance, and Runsite don't put a per-GB line on this app. What egress pricing does to storage-heavy apps is covered in [S3 egress fees](https://runsite.app/blog/s3-egress-fees).
* Seats. Render's step from Hobby to the $25 Pro workspace and Vercel's $20 per deploying seat are the only reason the team column differs from the solo column. Heroku, DigitalOcean, Fly.io and Runsite don't charge for people.
* The database floor. The smallest managed Postgres ranges from $5 on Heroku to $38 on Fly.io. On a small app the database decides the ranking more than anything else.
* The production-features jump. Heroku's cheap Basic dyno and Render's Hobby workspace are real products, but several features teams expect in production (horizontal scaling, previews, longer recovery windows) sit a tier up, and that tier costs three or four times as much.

Price the workload, not the plan name

Before comparing, pull the actual CPU, memory and bandwidth your app used last month from whatever metrics you have. Price that on each platform, one line per service, and add seats if more than one person deploys. That total is comparable. The plan names never are.

## Cheapest PaaS is the wrong question

The cheapest PaaS in this table depends on which column you read. Heroku wins the dollar total with a 25 MB cache and a platform in maintenance mode. Railway wins for an app that is mostly idle and loses for one that isn't. Fly.io wins on compute and egress and loses on the database. A comparison that crowns one of them without saying for which app is choosing the column that suits its conclusion.

The more useful question is which line on your bill will grow as the app grows. For an app that serves a lot of traffic, it's bandwidth. For a team that is hiring, it's seats. For anything with real data, it's the database. Pick the platform whose cheap line is your expensive one.

## How Runsite handles it

The reference app on Runsite is a Starter [web service](https://runsite.app/services/web-services) at €5/mo (0.5 vCPU, 512 MB, 1 TB of bandwidth), a Nano [PostgreSQL](https://runsite.app/services/postgresql) at €3/mo (0.1 vCPU, 256 MB, 1 GB of storage, daily backups) and a Starter [Redis](https://runsite.app/services/redis) at €5/mo (256 MB, with persistence and failover on every plan). That's €13 a month, and it stays €13 for a team of three because there are no seat fees and no per-GB bandwidth line on these plans. Everything is billed by the hour from a prepaid balance, so a service you stop halfway through the month costs half. Every plan and its hourly rate is on the [pricing page](https://runsite.app/pricing).

The limits, so the comparison stays fair. There is one region, in Germany, so an app with users in Singapore will serve them from Europe. The Nano web plan (€3/mo) sleeps after 14 days without traffic; Starter and above don't. Runsite is operated by an individual developer rather than a company, which matters if a procurement review needs an EU company registration number. What you get for that is data that stays in the EU, a signed [data processing agreement](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa) with every account, and a bill in euros that you can predict before the month starts. New accounts get €5 of credit.

## The short version

For a small production app with Postgres, Redis and 100 GB of traffic, the published prices on 26 September 2026 give about $15 on Heroku, about $17 on Railway (an estimate), $36 on DigitalOcean App Platform, $37 on Render and about $43 plus Redis on Fly.io. Vercel doesn't host this app on its own. Runsite is €13\. For a team of three, Render rises to $59 and Railway to its $20 minimum, and the rest don't change.

The web service is rarely what makes one platform expensive. Bandwidth, seats and the smallest managed database are. Price your own app's usage on each platform, one line per service, and check which of those three lines will grow with you.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### How much is Render per month?

It depends on how many services you run and how much traffic they serve. As of 26 September 2026, Render charges a workspace fee ($0 on Hobby for one person, $25/mo on Pro for teams) plus compute for each service. The common starting web service is $7/mo for 0.5 CPU and 512 MB, the smallest paid Postgres is $6/mo, and the smallest paid Key Value is $10/mo. Bandwidth beyond the included 5 GB (Hobby) or 25 GB (Pro) is $0.15 per GB. A small app with a web service, Postgres, a cache and 100 GB of traffic comes to about $37/mo for one developer and about $59/mo for a team on Pro.

### Is Heroku still free?

No. Heroku ended its free dynos, free Postgres and free Redis in November 2022\. The cheapest options now are Eco dynos at $5/mo for a shared pool of hours and Basic dynos at $7/mo, with Heroku Postgres Essential-0 at $5/mo and the Mini Key-Value Store at $3/mo. Since February 2026 Heroku has been in sustaining engineering under Salesforce: existing customers can keep using and renewing it at the same prices, but no new features are being built and Enterprise contracts are no longer sold to new customers.

### What is the cheapest PaaS?

It depends on the app. For a small production app with a web service, Postgres, a cache and 100 GB of traffic, Heroku has the lowest dollar total at about $15/mo as of 26 September 2026, but with a 25 MB cache and on a platform in sustaining engineering. Railway can be cheaper for an app that is idle most of the time, because it bills only the CPU and memory used, and more expensive for a busy one. Fly.io has the cheapest compute and the most expensive managed Postgres. The honest way to find the cheapest option is to price your own app's usage on each platform, including bandwidth, seats and the database, rather than comparing plan names.

### Is Railway cheaper than Render?

For an app that idles, usually yes. For an app that runs busy all day, often not. Railway bills RAM and CPU by the second at roughly $10 per GB and $20 per vCPU per month, plus $0.05 per GB of egress, with a $5 minimum on Hobby. Render charges a fixed $7/mo for a 0.5 CPU, 512 MB instance whether it is busy or not, plus $0.15 per GB of bandwidth beyond the included allowance. For the small reference app in this article, Railway comes to about $17/mo and Render to about $37/mo, most of the difference being Render's bandwidth charge. Railway's databases are unmanaged templates, though, so the comparison is not quite like for like.

Keep reading

---

# GitHub Webhook Timed Out? Why Your Host Drops Deliveries

> GitHub waits 10 seconds, marks the delivery "Timed out" and never retries. How sleeping apps, serverless limits and deploys lose webhooks, and the fix.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. GitHub Webhook "Timed out": Why Your Host Drops Deliveries

DeploymentSeptember 29, 20269 min read

# GitHub Webhook "Timed out": Why Your Host Drops Deliveries

GitHub gives your endpoint 10 seconds and never retries on its own. Where those seconds go on a sleeping free tier, a serverless function or a deploy, and how to build a receiver that doesn't lose events.

## The short version

GitHub expects a 2xx response within 10 seconds. If it doesn't get one, it closes the connection, marks the delivery "Timed out" and does not try again. A receiver that sleeps, restarts during deploys or does the work inside the request will lose events. The fix is an always-on endpoint that replies straight away and leaves the work to a queue.

[Teo Marquardt](https://runsite.app/about#author)· Facts last checked September 29, 2026

The Recent Deliveries tab on your repository's webhook settings is full of red, and your application logs show nothing. No errors, no stack traces, not even a request. From GitHub's side the delivery failed. From your app's side it never happened. That gap almost always comes from where the receiver runs, not from the code that handles the payload.

This article is about webhooks that GitHub sends to your server: push, pull request, release and the rest. If you arrived here from a Kubernetes admission webhook timing out, that is a different mechanism with a similar name, and none of what follows applies to it.

## GitHub webhook timeout: "Timed out" means GitHub waited 10 seconds and hung up

A GitHub webhook times out when your server doesn't return a 2xx status within 10 seconds. GitHub's own guidance is short: [your server should respond with a 2XX response within 10 seconds](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks) of receiving a delivery. If it takes longer, GitHub terminates the connection and records the delivery as a failure. In the delivery log that shows up as `Timed out`, and in some views as "timed out after 10s".

Ten seconds sounds generous for returning a status code, and for the handler alone it is. The catch is that those seconds cover more than your handler. They include resolving and reaching your host, the TLS handshake, any proxy or load balancer in front of the app, the app itself becoming ready if it wasn't, and only then your code. On a warm, always-on service the overhead is a few milliseconds. On some hosting setups it is most of the budget before your first line runs.

## Does GitHub retry failed webhook deliveries? No

No. [GitHub does not automatically redeliver failed webhook deliveries](https://docs.github.com/en/webhooks/using-webhooks/handling-failed-webhook-deliveries), so a delivery that ended in `Timed out` or `Failed to connect to host` is not tried again. Stripe and Shopify retry a failed webhook for hours or days with backoff. GitHub doesn't, and that is what turns a slow endpoint into lost data.

What you get instead is manual redelivery. You can resend any delivery [from the past three days](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/redelivering-webhooks), either with the Redeliver button in the web interface or through the REST API, which exists for repository, organisation and GitHub App webhooks. After three days the delivery can't be recovered from GitHub at all.

A failed delivery is permanent unless you act

One timeout on a Friday evening, noticed on Tuesday, is an event you will never get back from GitHub. Nothing alerts you by default. The only record is the red entry in Recent Deliveries.

For a walkthrough of how deliveries, headers and redelivery work on GitHub's side, including the event types and payload limits, see this [complete guide to GitHub webhook events, payloads and redelivery](https://webhooker.eu/blog/github-webhooks-guide). This article stays on the hosting side of the problem.

## Four places a host eats your 10 seconds

### A free tier that went to sleep

Many free hosting tiers stop a container after about 15 minutes without traffic, and the next request waits for it to start again. Render's own dashboard warns that a sleeping free instance can delay requests by 50 seconds or more; the mechanics are in [what cold starts are and why free tiers sleep](https://runsite.app/blog/cold-starts). GitHub will not wait 50 seconds. It will not wait 11.

What makes this failure hard to spot is its pattern. A repository with steady activity keeps the receiver awake, so deliveries during a busy afternoon all succeed. The first push after lunch, or the first on a Monday, arrives at a stopped container, times out, and is gone. The request that woke the app never reached your code, so the logs are clean. The next push works fine because the container is now running. It looks random, and it isn't.

### Serverless functions: a cold start plus a time limit

A function platform has the same wake-up cost when a function hasn't run recently, usually shorter than a container's but not zero. It also adds a second constraint: an execution cap. Handlers that verify the payload, then call the GitHub API, then write to a database, can hit the platform's limit on a slow day and be killed mid-request.

There is a subtler problem on many function runtimes. Once the response is sent, the runtime is free to freeze or discard the instance, so work you start "in the background" after replying may never finish. The delivery shows as successful in GitHub and the job silently doesn't happen, which is worse than a red entry you can at least see.

### The deploy window: why you get "Failed to connect to host"

A receiver that runs as a single instance and is restarted on every deploy has a few seconds, sometimes more, in which nothing is listening. A delivery that lands in that window doesn't time out. It fails outright, and GitHub reports it as one of these:

* `Failed to connect to network`: your server actively refused the connection, which is what a host with no process on the port does. It's the same reset your own code sees as [ECONNREFUSED](https://runsite.app/blog/econnrefused-127-0-0-1).
* `Failed to connect to host`: GitHub couldn't resolve the hostname or reach it at all, for example during DNS changes or behind a firewall that blocks it.
* `Invalid HTTP response`: something answered with a 4xx or 5xx, which during a deploy is usually the platform's proxy returning 502 or 503 because no healthy instance is behind it yet. The proxy's error log says which; [reading a 502 Bad Gateway in nginx](https://runsite.app/blog/502-bad-gateway-nginx) covers the lines you will find there.

Pushes trigger deploys, and deploys restart the receiver, so this window is aligned with exactly the moments when GitHub is most likely to be sending something. A release process that starts the new instance, waits for a health check and only then moves traffic closes the gap; the mechanics are in [blue-green and rolling deployments](https://runsite.app/blog/blue-green-vs-rolling-deployments).

### Doing the work inside the request

The last cause is on the application side, but the hosting shape usually encourages it. A handler that receives a push and then clones the repository, runs a build, posts a comment on the pull request or sends notifications will pass on a small repository and time out on a large one. [An old Stack Overflow question](https://stackoverflow.com/questions/46943006/send-response-but-keep-long-running-script-going-to-prevent-timeout) asks the same thing about a GitHub webhook that starts a long-running script: how to answer the sender straight away and let the script keep going.

## "Failed to connect to host" and other GitHub webhook errors, decoded

Each failed delivery in the log carries a short error string. Mapped to what was happening on your side, they read like this:

| What GitHub shows                                                   | What happened on your side                                                                                         | Where to look                           |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| Timed out                                                           | No response within 10 seconds: the app was asleep, cold, or busy doing the work in the request                     | Plan sleep settings, handler duration   |
| Failed to connect to network                                        | Connection refused: nothing was listening, typically between the old instance stopping and the new one starting    | Deploy logs at the delivery's timestamp |
| Failed to connect to host                                           | The hostname didn't resolve or the host was unreachable                                                            | DNS records, firewalls, IP allowlists   |
| Invalid HTTP response                                               | A 4xx or 5xx came back: a 502/503 from the platform proxy during a restart, or your own code rejecting the payload | Proxy logs, then app logs               |
| Peer certificate cannot be authenticated with given CA certificates | Self-signed or incomplete TLS certificate chain                                                                    | Certificate setup on the custom domain  |

Error strings as listed in GitHub's webhook troubleshooting documentation, as of September 2026.

Two further notes from [GitHub's troubleshooting page](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/troubleshooting-webhooks): deliveries can arrive out of order or several minutes late, and GitHub may throttle them during traffic surges. Neither is a hosting fault, but both matter for how you process events once you have them.

## Acknowledge first, work later

The pattern GitHub itself recommends is to separate receiving from processing. The handler does three things only: checks the signature, stores the delivery somewhere durable, and replies. Everything else happens in a worker that reads from the queue at its own pace. The response then takes milliseconds regardless of how heavy the job is, and a slow job can no longer cause a timeout.

javascript

```
import express from "express";
import crypto from "node:crypto";
import { createClient } from "redis";

const redis = await createClient({ url: process.env.REDIS_URL }).connect();
const app = express();

app.post("/github", express.raw({ type: "application/json" }), async (req, res) => {
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");
  const received = req.get("X-Hub-Signature-256") ?? "";
  if (expected.length !== received.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
    return res.sendStatus(401);
  }

  const deliveryId = req.get("X-GitHub-Delivery");
  // A redelivery reuses the original ID, so this also drops duplicates.
  const isNew = await redis.set(`gh:seen:${deliveryId}`, "1", { NX: true, EX: 259200 });
  if (isNew) {
    try {
      await redis.lPush("gh:deliveries", JSON.stringify({
        id: deliveryId,
        event: req.get("X-GitHub-Event"),
        payload: req.body.toString("utf8"),
      }));
    } catch (error) {
      await redis.del(`gh:seen:${deliveryId}`);
      return res.sendStatus(500);
    }
  }

  res.sendStatus(202);
});

app.listen(process.env.PORT ?? 3000);
```

A few details in that sketch are deliberate. The body is read raw, because the signature is computed over the exact bytes GitHub sent and a parsed-then-reserialised body won't match; the full reasoning, including secret rotation, is in this guide to [verifying GitHub webhook signatures](https://webhooker.eu/blog/verify-github-webhook-signature). The `X-GitHub-Delivery` header is unique per event and stays the same when a delivery is resent, which makes it a natural deduplication key; the key expires after three days to match GitHub's redelivery window, and it is removed again if the queue write fails, so a later redelivery isn't mistaken for a duplicate. The sketch is an ES module, which is what allows the top-level `await`. Why dedupe at all, and what to do when two deliveries race, is covered in [idempotency keys for webhook consumers](https://webhooker.eu/blog/webhook-idempotency-keys).

### Why the queue has to live outside the process

The tempting shortcut is to reply 202 and then run the job on a background thread or an unawaited promise in the same process. It works until the next deploy. When the platform replaces the container, whatever that thread was doing is lost, along with any jobs queued in memory behind it. The delivery is green in GitHub, so nobody will ever resend it.

A Redis list, as above, or a jobs table in PostgreSQL both survive a restart of the receiver. The worker is a separate long-running process that pulls from that queue, so deploying the web endpoint doesn't interrupt jobs in flight. The choice between a queue, a scheduled job and an always-on worker is laid out in [cron jobs, background workers and queues](https://runsite.app/blog/cron-jobs-workers-queues); a GitHub webhook receiver is the textbook case for the queue.

## A redelivery sweep for the misses you'll still have

An always-on receiver with a queue makes timeouts rare, not impossible. A certificate renewal that goes wrong, or an outage on GitHub's side, will still drop the odd delivery. Because GitHub won't retry, the safety net has to be yours: a scheduled job that lists recent deliveries for the hook, finds the failed ones and asks GitHub to resend them. GitHub's docs describe this approach and note that the script [should run on a schedule](https://docs.github.com/en/webhooks/using-webhooks/handling-failed-webhook-deliveries).

bash

```
# List recent deliveries for one repository webhook and resend the failed ones.
# OWNER, REPO and HOOK_ID are yours; gh must be authenticated with admin:repo_hook.
gh api "repos/OWNER/REPO/hooks/HOOK_ID/deliveries?per_page=100" \
  --jq 'group_by(.guid) | map(select(all(.[]; .status != "OK"))) | .[] | .[0].id' |
while read -r delivery_id; do
  gh api -X POST "repos/OWNER/REPO/hooks/HOOK_ID/deliveries/$delivery_id/attempts"
done
```

Run it every hour or so, well inside the three-day window. The filter groups attempts by the delivery's GUID and only picks deliveries where no attempt has succeeded, so a miss is resent once rather than on every run. A production version should also page through results and log what it resent. The deduplication in the handler is what makes the sweep safe to run often: a redelivered event with a known `X-GitHub-Delivery` is dropped instead of processed twice.

## How Runsite handles it

The receiver is a small [web service that stays up between deliveries](https://runsite.app/services/web-services). On Runsite an active service doesn't cold-start, including on the €3/mo Nano plan, which sleeps only after 14 days with no requests at all; Starter (€5/mo) and above never sleep. A repository that pushes once a week still hits a running container. Releases are rolling behind a health check you configure, so the new version takes traffic only once it reports healthy and the old one keeps answering until then, which removes the deploy-window failures from the table above.

The worker runs as a second service from the same repository that doesn't serve HTTP, started at deploy and restarted with the app. The queue between them can be [managed Redis](https://runsite.app/services/redis) from €5/mo or a table in [managed PostgreSQL](https://runsite.app/services/postgresql) from €3/mo, and the redelivery sweep fits a [scheduled cron job](https://runsite.app/services/cron-jobs) with failure alerts by email or webhook. All of it runs in Germany. Worth knowing for GitHub payloads in particular, since they carry commit authors' names and email addresses: the [data stays in the EU](https://runsite.app/blog/gdpr-data-residency) under a signed data processing agreement.

## The short version

GitHub gives your endpoint 10 seconds to return a 2xx, then marks the delivery `Timed out`, `Failed to connect to host` or `Invalid HTTP response`, and never tries again on its own. Those seconds are spent before your code runs when the app is asleep, cold or mid-deploy. Host the receiver on something always-on, reply with 202 as soon as the signature checks out, push the payload to a queue outside the process, dedupe on `X-GitHub-Delivery`, and run a sweep that resends failures within three days.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Why do webhooks fail?

For GitHub webhooks, most failures come from the receiving side rather than from GitHub. The endpoint takes longer than 10 seconds to answer because the app was asleep on a free tier, cold on a serverless platform, or doing heavy work inside the request; or nothing is listening because the app is being restarted during a deploy; or a proxy returns a 502 or 503 while no healthy instance is available. Less often the cause is DNS, a firewall blocking GitHub, or an incomplete TLS certificate chain. The error string in the Recent Deliveries tab (Timed out, Failed to connect to network, Failed to connect to host, Invalid HTTP response) tells you which of these it was.

### How to handle webhook retries?

With GitHub you have to handle them yourself, because GitHub does not automatically redeliver failed deliveries. You can resend any delivery from the past three days with the Redeliver button or through the REST API for repository, organisation and GitHub App webhooks. A reliable setup runs a scheduled job that lists recent deliveries, finds the failed ones and requests a redelivery for each. Since a redelivery keeps the original X-GitHub-Delivery ID, the receiver should record the IDs it has already processed and drop repeats, so the job can run often without creating duplicate work.

### How long does GitHub wait for a webhook response?

Ten seconds. GitHub's documentation says your server should respond with a 2XX status within 10 seconds of receiving a delivery; after that GitHub terminates the connection and records the delivery as failed with the error Timed out. The time covers everything before the response, including a sleeping container waking up and any proxy in front of the app, so the practical budget for your own code is smaller. The usual fix is to acknowledge with a 202 immediately and process the payload from a queue.

### Can I redeliver a failed GitHub webhook?

Yes, within three days. Open the webhook's settings, go to Recent Deliveries, select the failed delivery and choose Redeliver, or call the REST API endpoint that creates a new delivery attempt for that delivery ID. The API covers repository, organisation and GitHub App webhooks. After three days the delivery can no longer be resent, and GitHub does not redeliver anything on its own, so a missed event is permanent unless someone or something resends it in time.

Keep reading

---

# 413 Request Entity Too Large: Find the Layer, Fix the Limit

> 413 Request Entity Too Large comes from one of several limits: CDN, proxy, ingress or framework. How to tell which one, and when not to raise it.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. 413 Request Entity Too Large: Which Layer Said No, and When to Stop Raising the Limit

DeploymentOctober 2, 20269 min read

# 413 Request Entity Too Large: Which Layer Said No, and When to Stop Raising the Limit

A 413 comes from whichever size limit in the request path is smallest: the CDN, the proxy, the ingress or the framework. How to tell which one answered, how to raise it, and when a large upload shouldn't go through your app at all.

## The short version

A 413 means some layer between the client and your code refused the request body because it was bigger than that layer allows. There are usually three or four such limits in a row, and the smallest one wins, so the fix starts with finding out which layer answered. For JSON and form bodies, raise that limit. For large files, stop sending them through the app and upload them straight to object storage with a presigned URL.

[Teo Marquardt](https://runsite.app/about#author)· Facts last checked October 1, 2026

Everything worked in testing. Then a real user uploads a 12 MB photo, or a client posts a JSON export a bit larger than the ones you tried, and the request fails with `413 Request Entity Too Large`. Your application logs are often empty, because in most cases the request never reached your code. Something in front of it read the `Content-Length`, compared it with a number in its config and refused.

The usual advice is one line: raise `client_max_body_size`. Sometimes that fixes it. Often it doesn't, because nginx was only one of several limits, and the next one down the chain answers with the same status code. This article is about finding the layer that actually said no, and about the case where raising the limit is the wrong answer.

## What "413 Request Entity Too Large" actually means

HTTP 413 means the server refuses to process a request because its body is larger than the server is willing or able to accept. The status has had three names. RFC 2616 called it `Request Entity Too Large`, RFC 7231 renamed it `413 Payload Too Large`, and [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-413-content-too-large), the current HTTP specification from 2022, calls it `413 Content Too Large`. The code and the meaning stayed the same. nginx and many frameworks still print the oldest name, which is why that's the phrase people search for.

The limit is about the body: the uploaded file, the JSON payload, the form data. Oversized headers or cookies are a different failure. RFC 6585 gives them their own status, `431 Request Header Fields Too Large`, and nginx answers an oversized header with a `400` instead. If you see a 413 on a request with a tiny body, check for a proxy that reports the wrong code before you go looking for size limits.

Retrying a 413 doesn't help

A 429 or a 503 can succeed on the next attempt. A 413 normally won't: the same body hits the same limit every time. The rare exception is a 413 that carries a `Retry-After` header, which signals a temporary condition. Client code that retries every 4xx with backoff should exclude 413 and surface it to the user, or switch to a different upload path. The retry logic for codes that do deserve it is in [rate limiting, 429 and Retry-After](https://runsite.app/blog/rate-limiting-429-retry-after).

## Whose 413 is it? Read the response body

Every layer that enforces a size limit formats its rejection differently, so the response body usually names the culprit. Open the failed request in your browser's network tab or repeat it with `curl -i` and look at what came back, not just the status line.

html

```
<html>
<head><title>413 Request Entity Too Large</title></head>
<body>
<center><h1>413 Request Entity Too Large</h1></center>
<hr><center>nginx</center>
</body>
</html>
```

That HTML page, with `<center>nginx</center>` at the bottom, comes from nginx itself. The ingress-nginx controller in Kubernetes produces the same page, because it is nginx underneath. A JSON body such as `{"message":"request entity too large"}` or a stack trace mentioning `PayloadTooLargeError` means the request got through every proxy and your framework rejected it. A branded error page with a ray ID or similar request identifier points at a CDN or edge proxy.

| Layer                      | Default limit (as of writing)                             | How its 413 looks                                      | Setting to change                           |
| -------------------------- | --------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------- |
| CDN or edge proxy          | Depends on plan; Cloudflare allows 100 MB on Free and Pro | Provider-branded error page                            | Plan level, often not configurable          |
| nginx                      | 1 MB                                                      | HTML page ending in <center>nginx</center>             | client\_max\_body\_size                     |
| ingress-nginx (Kubernetes) | 1 MB                                                      | Same nginx HTML page                                   | nginx.ingress.kubernetes.io/proxy-body-size |
| Apache httpd               | 1 GiB since 2.4.54                                        | Apache error page                                      | LimitRequestBody                            |
| Express express.json()     | 100 kb                                                    | PayloadTooLargeError: request entity too large         | limit option of the body parser             |
| Spring Boot multipart      | 1 MB per file, 10 MB per request                          | Framework error, often a 413 or 500 depending on setup | spring.servlet.multipart.\*                 |

Defaults from each project's documentation, as of October 2026\. Your platform may set its own values on top.

### When the 413 shows up as a CORS error

In a browser, a 413 from a proxy often doesn't look like a 413 at all. The console reports a CORS failure instead. The cause is boring: your application adds the `Access-Control-Allow-Origin` header, but the proxy rejected the request before your application saw it, so its error page has no CORS headers, and the browser hides the response from your JavaScript. If uploads above a certain size fail with a CORS error while small ones work, treat it as a size limit first. Some proxies also close the connection before reading the whole body, in which case the browser reports a reset connection instead of any status.

## The limits, from the outside in

A request is checked against every size limit on its path, and the smallest one wins. Each layer compares the body with its own number, in order, from the edge inwards. Raise them from the outside in, and remember that the inner ones only matter once the outer ones let the request through.

### CDN and edge proxies

If your domain sits behind a CDN that proxies requests, the CDN has a body size ceiling of its own. On Cloudflare it's tied to the plan, at [100 MB on Free and Pro](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/4xx-client-error/error-413/) and higher on Business and Enterprise. You usually can't raise it in config. For files that large the way around it is to not send them through the proxied domain at all, which is the subject of the section on presigned uploads below.

### nginx: client\_max\_body\_size

nginx's [client\_max\_body\_size](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fcore%5Fmodule.html#client%5Fmax%5Fbody%5Fsize) defaults to `1m`, which is why a reverse proxy with an untouched config rejects the first realistic upload. The directive works in the `http`, `server` and `location` contexts. Setting it to `0` turns the check off, which is rarely what you want on a public endpoint.

nginx

```
server {
    server_name app.example.eu;

    # Applies to the whole site
    client_max_body_size 10m;

    location /api/uploads {
        # Larger limit only where uploads are expected
        client_max_body_size 50m;
        proxy_pass http://app:3000;
    }
}
```

Scoping the larger value to the upload route keeps the rest of the site protected from someone posting a gigabyte of junk to your login form.

### Kubernetes ingress: proxy-body-size

In a cluster with ingress-nginx, editing an nginx.conf won't help, because the controller generates it. The limit is set per Ingress with an annotation, and it also defaults to 1 MB.

yaml

```
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
```

If there's also an nginx inside the application container, it keeps its own `client_max_body_size`. Two nginx instances in a row means two limits to raise, and the 413 page looks identical whichever one sent it. A reply on r/nginx sums it up: the 413 can come from the reverse proxy or from the backend, so check the config on both. In practice it's often both.

### Express and NestJS: PayloadTooLargeError

Once the proxies let the body through, the framework gets its turn. Express's built-in JSON parser accepts [100 kb by default](https://expressjs.com/en/api.html#express.json), which is far less than people expect. A rich-text document or an array of a few thousand records goes over it easily, and the log shows this:

text

```
PayloadTooLargeError: request entity too large
    at readStream (/app/node_modules/raw-body/index.js)
    at getRawBody (/app/node_modules/raw-body/index.js)
    at read (/app/node_modules/body-parser/lib/read.js:79:3)
```

javascript

```
// Raise the limit for JSON bodies only, and only as far as you need
app.use(express.json({ limit: "2mb" }));
app.use(express.urlencoded({ extended: true, limit: "2mb" }));
```

NestJS uses the same body parser under its Express adapter, so the error text is identical and the fix is the same option, passed when the app is created. File uploads in Express normally go through a multipart middleware such as multer instead, which has its own `limits.fileSize` setting, unset by default, and ignores the JSON limit.

### Django, Flask, Spring Boot and PHP

* Django limits non-file request data with `DATA_UPLOAD_MAX_MEMORY_SIZE`, 2.5 MB by default. Going over it raises `RequestDataTooBig`, which Django answers with a 400, not a 413\. If nothing in your stack sends a 413, that's why.
* Flask has no overall body limit unless you set `MAX_CONTENT_LENGTH`. Once set, oversized requests get a 413 from Werkzeug's `RequestEntityTooLarge`. Recent versions also cap in-memory form fields with `MAX_FORM_MEMORY_SIZE`.
* Spring Boot caps multipart uploads at 1 MB per file and 10 MB per request through `spring.servlet.multipart.max-file-size` and `max-request-size`.
* PHP has `upload_max_filesize` and `post_max_size`. PHP itself doesn't answer with a 413 when they're exceeded. Past `post_max_size`, `$_POST` and `$_FILES` arrive empty; past `upload_max_filesize`, the file's entry carries the error `UPLOAD_ERR_INI_SIZE`. Both are more confusing than a status code. The 413 people see on PHP sites almost always comes from the nginx in front.

## "I raised client\_max\_body\_size and still get 413"

This is the most common follow-up question, and it usually comes down to one of four things:

1. The config wasn't reloaded. Run `nginx -t` to check the syntax, then `nginx -s reload` or restart the container. A changed file on disk does nothing on its own.
2. The directive is in the wrong block. A value in one `server` block doesn't apply to another, and a more specific `location` with its own smaller value overrides the server-wide one. Search the full config, including included files, for every occurrence.
3. It's a different nginx. The one you edited may be the inner one, while an ingress, a load balancer or a platform proxy in front still has the default. The response body is identical, so check each hop.
4. It's not nginx at all. If the 413 now comes back as JSON or a framework error, nginx is fixed and the next limit is in your application.

## When raising the limit is the wrong fix

For JSON payloads and ordinary forms, raising the limit to a sensible value is the right move. A 5 MB JSON body is unusual but not dangerous. Files are different. Once uploads reach tens or hundreds of megabytes, every layer in the path has to cope with them, and raising one limit tends to expose the next problem.

* A web process receiving a 500 MB video is tied up for as long as the upload takes, which on a slow mobile connection can be many minutes.
* Proxies and load balancers have read timeouts of their own, so a large slow upload can fail with a 408 or 504 even after every size limit has been raised.
* Depending on the framework, the body is buffered in memory or written to a temporary disk on the instance, both of which are limited on a container.
* If the file ends up in object storage anyway, it crosses the network twice: from the user to your app, then from your app to the bucket.

The fix that removes the problem is to let the browser upload the file straight into the bucket. Your app signs a short-lived [presigned URL](https://runsite.app/blog/s3-presigned-urls) for one object, hands it to the client, and the client sends the file directly to storage. Your web process only handles a small JSON request for the URL and, later, a confirmation. The size limit doesn't go away. It moves into the signature. A presigned PUT pins one exact `Content-Length`, and a presigned POST can carry a `content-length-range` condition on providers that support POST policies; whether yours does is worth checking, as the presigned URL guide explains. For files in the hundreds of megabytes and up, split the upload into parts; the details of [multipart uploads and what to retry](https://runsite.app/blog/upload-files-to-s3) are a topic of their own.

| What's in the request body         | Typical size     | What to do about a 413                                                            |
| ---------------------------------- | ---------------- | --------------------------------------------------------------------------------- |
| JSON API payload                   | KB to a few MB   | Raise the framework's body limit to a realistic maximum, plus the proxy if needed |
| HTML form with fields              | KB               | Raise the form limit; if it's still too big, look at what the form is sending     |
| Avatar or document upload          | Up to \~10 MB    | Either path works; raising limits on one route is fine                            |
| Video, archives, datasets, backups | Tens of MB to GB | Upload directly to object storage with a presigned URL, multipart for large files |

## How Runsite handles it

On Runsite the app and the bucket live next to each other. You [deploy the web service](https://runsite.app/services/web-services) from Git and set the body limits that belong to your application, in your framework config, the same way you would anywhere else. Most hosting platforms also put a request size limit in front of your service, so if you hit a 413 you can't explain from your own config, check your platform's documentation for that limit before raising anything else.

For anything large, [S3-compatible object storage in the EU](https://runsite.app/services/s3-storage) works with the standard AWS SDKs, presigned URLs included, so the browser can upload straight to the bucket and your web process never carries the file. Objects can be up to 5 TB, with multipart recommended above 100 MB. Storage costs €0.025 per GB a month after the first 5 GB, with no charge for transfer in or out. The bucket is hosted in Germany, next to the services that use it.

## The short version

* `413 Request Entity Too Large` (now officially `413 Content Too Large`) means a layer refused the request body because it exceeded that layer's limit.
* There are usually several limits in a row: CDN, proxy, ingress, framework. The smallest one wins, and the response body tells you which one answered.
* nginx and ingress-nginx default to 1 MB, Express's JSON parser to 100 kb. Raise them from the outside in, and reload after every change.
* Don't retry a 413\. The same body gets the same answer.
* For large files, stop raising limits and upload directly to object storage with a presigned URL.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### How can I fix the HTTP 413 error?

First find out which layer sent it. An HTML page ending in "nginx" means nginx or ingress-nginx, where you raise client\_max\_body\_size or the proxy-body-size annotation and reload. A JSON error or PayloadTooLargeError means your framework, where you raise the body parser's limit. A branded error page means a CDN, whose limit is usually tied to your plan. Raise each limit only as far as your real maximum request needs. If the bodies are large files, upload them directly to object storage with a presigned URL instead of raising limits further.

### What does the 413 error code mean?

HTTP 413 means the server refused to process the request because the request body is larger than it is willing or able to accept. RFC 9110 names it 413 Content Too Large; older specifications called it Payload Too Large and Request Entity Too Large, which is the text nginx still shows. It is a client error: unless the response carries a Retry-After header, sending the same request again gives the same result, so the body has to get smaller, the server's limit has to go up, or the data has to take a different route such as a direct upload to storage.

### What is meant by request entity too large?

"Request entity" is the old HTTP term for the body of a request, such as an uploaded file, a JSON payload or submitted form data. "Request entity too large" means that body was bigger than a limit configured somewhere between the client and the application, for example nginx's client\_max\_body\_size, which defaults to 1 MB, or Express's JSON parser limit, which defaults to 100 kb. It says nothing about headers or URLs, which have their own limits and status codes.

### What does the API error 413 mean?

In an API, a 413 means the JSON or file you sent exceeded the size the API or its proxy accepts. With a Node.js backend the log usually shows PayloadTooLargeError: request entity too large, because Express's JSON parser accepts only 100 kb by default. If you own the API, raise the parser limit to your realistic maximum, and the proxy limit if one sits in front. If you're calling someone else's API, check its documented size limit and split the payload into smaller requests or use its file-upload endpoint.

Keep reading

---

# Exit Code 137: OOMKilled or Just SIGKILL? How to Tell

> Exit code 137 means SIGKILL, and memory is only one cause. How to tell an OOM kill from a missed SIGTERM on deploy, and how to fix each one.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. Exit Code 137: SIGKILL, Not Always Out of Memory

DeploymentOctober 5, 202610 min read

# Exit Code 137: SIGKILL, Not Always Out of Memory

Exit code 137 says a process was killed with SIGKILL. It doesn't say who sent it. How to tell a memory kill from a missed SIGTERM on deploy or a failing probe, and what fixes each one.

## The short version

Exit code 137 is 128 plus 9: the process was killed with SIGKILL, which it cannot catch. The kernel sends it when a container hits its memory limit, but a runtime also sends it when a process ignores SIGTERM for too long, and probes or CI runners send it too. Check the OOMKilled flag first, then fix memory or shutdown handling.

[Teo Marquardt](https://runsite.app/about#author)· Facts last checked October 2, 2026

The container is gone. The application log stops mid-line, with no stack trace and no error, and the only trace left is a status in `docker ps -a`: `Exited (137)`. Most answers you'll find say the same thing, that the container ran out of memory and needs a bigger limit. Often that's right. When it isn't, raising the memory changes nothing, and the container keeps dying with the same code.

The number tells you less than people assume. This article goes through the three different things that produce a 137, how to tell them apart from the container's state in under a minute, and the fix for each.

## What exit code 137 means

When a process is ended by a signal, the shell and container runtimes report its exit status as 128 plus the signal number. [Bash documents the rule](https://www.gnu.org/software/bash/manual/html%5Fnode/Exit-Status.html) and Docker and Kubernetes follow it. Signal 9 is SIGKILL, so 128 + 9 = 137\. A process can't catch, block or ignore SIGKILL, and the kernel removes it on the spot. Nothing in your code runs after that, not a `finally` block and not the last buffered log line, which is why the log just stops.

| Exit code | Signal       | What it usually means                                                                       |
| --------- | ------------ | ------------------------------------------------------------------------------------------- |
| 137       | SIGKILL (9)  | Killed outright. Memory limit, an expired grace period, a probe, or someone running kill -9 |
| 143       | SIGTERM (15) | Asked to stop and stopped. Normal during deploys and scale-downs                            |
| 139       | SIGSEGV (11) | Segmentation fault, usually in native code or a broken binary                               |
| 1         | none         | The application exited with an error by itself. Read its log                                |
| 0         | none         | Clean exit, including a SIGTERM handler that finished and called exit                       |

In a container, three senders cover nearly every case: the kernel enforcing a memory limit, the runtime giving up on a process that didn't stop when asked, and some other component that decided the container had to go.

## Exit code 137 but OOMKilled is false: read the container state first

The same code shows up as docker exit code 137 on a plain host and as kubernetes exit code 137 in a pod's last state, and the first check is the same. Before changing anything, ask the runtime what it recorded. Docker keeps an `OOMKilled` flag next to the exit code. Example output:

bash

```
$ docker inspect --format '{{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} finished={{.State.FinishedAt}}' api
137 OOMKilled=false finished=2026-10-01T14:02:41.118Z
```

Kubernetes shows the same information as the reason on the container's last state, which is where `Reason: OOMKilled` comes from. Example output:

text

```
$ kubectl describe pod api-7c9f8d6b5-x2kqp
...
    State:          Running
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
    Restart Count:  4
```

`Reason: OOMKilled` means the memory limit did it. `Reason: Error` with `Exit Code: 137` means something else sent the SIGKILL. That's the Kubernetes version of the "137 but OOMKilled is false" threads on the Docker forums. If you have access to the host, the kernel log settles it. Example output:

text

```
$ dmesg -T | grep -i 'killed process'
[Thu Oct  1 14:02:41 2026] Memory cgroup out of memory: Killed process 4127 (node) ...
```

`Memory cgroup out of memory` means the container hit its own limit. A line starting with just `Out of memory: Killed process` means the whole machine ran out, and the kernel picked a victim, possibly yours. The exact wording varies a little between kernel versions, so search for "Killed process" rather than the full line. `journalctl -k` shows the same log on systemd hosts.

| What you see                                                    | Who sent the SIGKILL                           | Where to look next                      |
| --------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------- |
| OOMKilled: true or Reason: OOMKilled                            | The kernel, at the container's memory limit    | Memory graph, runtime heap settings     |
| OOMKilled: false, died during a deploy, restart or scale-down   | The runtime, after the grace period ran out    | SIGTERM handling and what runs as PID 1 |
| OOMKilled: false, Out of memory: Killed process in the host log | The kernel, short of memory on the whole host  | Host memory and the neighbours on it    |
| Reason: Error, failed liveness probe in the pod events          | The kubelet, restarting an unhealthy container | Probe timing and slow startup           |
| Killed at the same point in every CI run                        | The runner, on a memory or time limit          | The job's resource limits               |

One caveat on the flag. In some setups `OOMKilled` stays `false` after a real memory kill, for example when the host ran out of memory rather than the container. Treat the flag as strong evidence when it's `true` and weak evidence when it's `false`. If the timing doesn't match a deploy and nothing else fits, check the kernel log anyway.

## Cause 1: the memory limit (OOMKilled: true)

A container's memory limit is enforced by the kernel through cgroups. When the processes inside it try to use more, the kernel's OOM killer picks a process in that group and sends it SIGKILL. There's no warning signal first, and nothing for your code to catch. If your main process is the one picked, the container exits with 137 and the runtime records `OOMKilled`.

### Your runtime doesn't know about the container limit

The most common cause isn't a leak. It's a runtime that sizes its heap without regard to the limit it's running under, and then grows into it.

* **Node.js.** V8 picks a maximum heap size on its own, and depending on the version that number may be based on more memory than your container has. Set it explicitly with `--max-old-space-size` (in megabytes), at roughly three quarters of the container limit so buffers and native memory have room. On a 512 MB plan that means something like `node --max-old-space-size=384 server.js`.
* **Java.** JVMs have read container limits since JDK 10, and 8u191 on Java 8, but the default heap is only a quarter of that limit (more on very small containers). Tune it with `-XX:MaxRAMPercentage` instead of a fixed `-Xmx`, so the setting follows the plan size if you change it.
* **Python and Ruby.** Neither has a heap ceiling at all. Memory grows until the limit is hit. Worker count matters more than anything: four Gunicorn or Puma workers each loading the full app use four times the memory of one.

### 137 during the build, not at runtime

If the 137 shows up in the build log, the application never started. `pip install` compiling a package with native extensions, `next build`, a webpack production build or `yarn install` on a large monorepo can all need more memory than the running app ever will. The fix there is the build's memory, or a lighter build step (prebuilt wheels, a narrower bundle), not the runtime plan.

### Raise the limit or find the leak

The memory graph decides this. A sawtooth that rises and drops again is a garbage collector doing its job; if the peaks touch the limit, the limit is too small for the work, and a bigger plan is the honest fix. A staircase that climbs steadily over hours and never comes back down is a leak, and a bigger limit only moves the 137 a few hours later. A flat line at the limit followed by a drop to zero is the kill itself.

## Cause 2: SIGTERM was ignored, so SIGKILL followed

This is the 137 that no amount of memory fixes, and the pattern gives it away: it happens on deploys and restarts and never in between. Stopping a container is a two-step process. The runtime sends SIGTERM and waits. If the process is still running when the grace period ends, it sends SIGKILL. [docker stop waits 10 seconds by default](https://docs.docker.com/reference/cli/docker/container/stop/), Compose uses the same default through `stop_grace_period`, and [Kubernetes waits 30 seconds](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) through `terminationGracePeriodSeconds`.

A process that exits on SIGTERM never reaches the second step. A process that doesn't hear SIGTERM, or hears it and keeps going, sits there until the grace period ends and then goes out with 137\. During a [rolling or blue-green release](https://runsite.app/blog/blue-green-vs-rolling-deployments) that happens to every old container being drained, which is why the code turns up in every deploy log.

SIGKILL can't be handled

Every fix for a 137 lives before the SIGKILL. Code can react to SIGTERM; nothing reacts to SIGKILL. A shutdown routine that needs 40 seconds under a 30-second grace period will be cut off every single time.

### The PID 1 problem

The first process in a container runs as PID 1, and Linux treats PID 1 differently: it gets no default signal actions, so a signal it has no handler for is simply ignored. A Node or Python app that never registered a SIGTERM handler would normally die on SIGTERM; as PID 1 it shrugs the signal off and waits for the SIGKILL.

The Dockerfile often makes this worse. The shell form of `CMD` runs your command through `/bin/sh -c`, so depending on the shell, PID 1 may be `sh`, with your app as a child that never gets the signal. `npm start` adds another layer of its own between the signal and your code.

dockerfile

```
# Shell form: runs under /bin/sh -c, and npm adds another layer.
# SIGTERM may never reach node.
CMD npm start

# Exec form: node is PID 1 and gets SIGTERM directly.
# It still needs a handler (below) or --init to act on it.
CMD ["node", "server.js"]
```

If you need a shell script as the entrypoint, end it with `exec node server.js` so the app replaces the shell instead of running under it. Another option is a minimal init process: [docker run --init](https://docs.docker.com/reference/cli/docker/container/run/#init), or `init: true` in Compose, puts `tini` in as PID 1, and tini forwards signals to your app and reaps zombie processes. Your app is then no longer PID 1, so a SIGTERM it has no handler for ends it the normal way.

### Handle SIGTERM and finish inside the grace period

Receiving the signal is half of it. The app then has to stop taking new work, let what's in flight finish, close its connections and exit, all before the grace period runs out. Requests cut off when it doesn't reach users behind nginx as a [502 Bad Gateway with "upstream prematurely closed connection" in the log](https://runsite.app/blog/502-bad-gateway-nginx).

javascript

```
const server = app.listen(process.env.PORT);
// db is your connection pool

process.on("SIGTERM", () => {
  console.log("SIGTERM received, draining");
  server.close(async () => {
    await db.end();
    process.exit(0);
  });
  // Node 18.2+: drop idle keep-alive sockets so close() can finish
  server.closeIdleConnections?.();
  // Leave on our own terms if draining takes too long.
  setTimeout(() => process.exit(1), 8000).unref();
});
```

The timeout has to sit below the grace period, with a margin. Background work needs the same treatment: a worker in the middle of a job should either finish it or put it back on the queue. A scheduled task that starts five seconds before a deploy is the classic victim, which is one more reason [where your cron schedule lives](https://runsite.app/blog/cron-job-multiple-instances) matters when several instances come and go.

### Exit code 143 or 0 means shutdown works

Once SIGTERM reaches the app, the deploy log changes. A process that isn't PID 1, for example one running under `--init`, and has no handler dies from SIGTERM and exits with 143\. One whose handler drains and calls `process.exit(0)` exits with 0\. Both mean the stop happened inside the grace period. Seeing `exit code 143` after a deploy is not an error to chase; 137 is the one that means requests were cut off.

## Cause 3: something else sent the SIGKILL

* A failing liveness probe. When a Kubernetes liveness probe fails often enough, the kubelet restarts the container through the same stop sequence, so an app that ignores SIGTERM goes out with 137 and `Reason: Error`. Slow startups are the usual culprit: the probe starts checking before the app is ready. A startup probe or a longer initial delay fixes it.
* The host itself running out of memory. If the machine runs out before your container reaches its own limit, the kernel kills whatever scores worst, and that can be a container that was well within its limit.
* A person or a script: `docker kill`, `kill -9` or a cleanup job sends SIGKILL directly, with no SIGTERM first.
* A CI runner. GitLab runners, GitHub Actions and Jenkins agents kill jobs that exceed their memory or time limits, and the job reports 137\. If it fails at the same step every time, the step needs more memory than the runner has.

### command terminated with exit code 137

This exact message comes from `kubectl exec` when the command you ran inside a pod was killed. Often the pod was already close to its memory limit, and your extra process (a database dump, a migration, a console session) pushed it over. The OOM killer picks the highest-scoring process in the container, which may be your command or the app itself, so check `kubectl describe pod` for a restart before blaming the command.

One that looks related but isn't: if your Redis log shows `OOM command not allowed`, Redis is refusing writes on purpose under its `maxmemory` setting, and nothing was killed. That's a [different error with a different fix](https://runsite.app/blog/redis-oom-command-not-allowed). A Redis container that exits with 137 is the opposite case, usually with `maxmemory` left at 0, so the kernel enforced the limit instead.

## How Runsite handles it

On Runsite every [web service runs on a plan with a fixed memory allowance](https://runsite.app/services/web-services), from 256 MB to 16 GB, and moving to a bigger plan is a setting, not a code change. Memory and CPU graphs sit in the dashboard next to the real-time logs, so you can tell a sawtooth from a staircase before the container dies. Containers that crash are restarted automatically, and health checks catch an app that is running but not answering.

Deploys are rolling: new containers start next to the old ones, traffic moves once the health checks pass, and the old containers are then stopped. As on any platform, there's a window between the stop request and a forced kill, so the rules above apply unchanged: exec-form `CMD`, a SIGTERM handler, and a shutdown that finishes in seconds. Get those right and the old version drains instead of being cut off mid-request. The services run in Germany, and their logs stay in the EU too.

## The short version

* Exit code 137 is 128 + 9: the process got SIGKILL, which it cannot catch. It tells you how the process died, not who killed it.
* Check `docker inspect` for `OOMKilled` or `kubectl describe pod` for `Reason: OOMKilled` before you change anything.
* If it was memory, set the runtime's heap below the container limit, then use the memory graph to tell a too-small limit from a leak.
* If it happens on every deploy, the app isn't stopping on SIGTERM. Use exec-form `CMD` or `--init`, handle SIGTERM, and finish inside the grace period.
* Exit code 143 or 0 on deploy means shutdown works. 137 means it ran out of time.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does exit code 137 mean?

Exit code 137 means the process was killed by signal 9, SIGKILL, because runtimes report a signal death as 128 plus the signal number. SIGKILL cannot be caught or ignored, so the process stops immediately without running any cleanup code. The most common sender is the Linux kernel when a container exceeds its memory limit, but a container runtime also sends SIGKILL when a process does not exit within the grace period after SIGTERM, and probes, CI runners or a manual kill -9 can send it too.

### What causes a container to exit with code 137?

Three things in practice. The container hit its memory limit and the kernel's OOM killer ended the main process, which Docker records as OOMKilled true and Kubernetes as Reason: OOMKilled. Or the container was being stopped, for a deploy, restart or scale-down, and the process ignored SIGTERM until the grace period ran out, usually because it ran as PID 1 without a SIGTERM handler or behind a shell or npm that didn't pass the signal on. Or another component killed it directly: a failing liveness probe, the host running out of memory, a CI runner limit, or docker kill.

### What does OOMKilled mean?

OOMKilled means the Linux out-of-memory killer terminated the container's process because the container tried to use more memory than its limit allowed. Docker shows it as State.OOMKilled set to true in docker inspect, and Kubernetes shows Reason: OOMKilled with exit code 137 in kubectl describe pod. The fix is either a larger memory limit, if the workload genuinely needs it, or a smaller footprint: a heap size set below the limit, fewer worker processes, or a fixed memory leak.

### How do I fix exit code 137?

Start by finding out which kind it was. If OOMKilled is true, set the runtime's heap below the container limit, for example with --max-old-space-size in Node or -XX:MaxRAMPercentage in Java, check the memory graph for a leak, and raise the limit if the workload needs it. In Kubernetes that means raising resources.limits.memory. If OOMKilled is false and the 137 happens on deploys or restarts, make the app stop on SIGTERM: use exec-form CMD, an init process such as tini, and a shutdown handler that finishes inside the grace period.

Keep reading

---

# ECONNREFUSED 127.0.0.1:6379 After Deploy: Why and How to Fix

> ECONNREFUSED 127.0.0.1 means your app is calling itself. Why it happens after deploy with Redis (6379) and Postgres (5432), and how to fix each cause.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. ECONNREFUSED 127.0.0.1: Your App Is Calling Itself

DeploymentOctober 7, 20269 min read

# ECONNREFUSED 127.0.0.1: Your App Is Calling Itself

Error: connect ECONNREFUSED 127.0.0.1:6379 after a deploy means your app is calling its own machine. Why the address says localhost, the ::1 variant, and how to fix each cause for Redis and Postgres.

## The short version

ECONNREFUSED means the target host answered and said nothing is listening on that port. When the address is 127.0.0.1 or ::1, your app is trying to reach a database on its own machine, usually because the connection string never reached the deployed environment, localhost was hardcoded, or the app runs in a container where localhost is the container itself. Set REDIS\_URL or DATABASE\_URL to the real host and fail loudly when it is missing.

[Teo Marquardt](https://runsite.app/about#author)· Facts last checked October 5, 2026

It worked on your laptop. You deployed, the app started, and the first request to the cache produced this:

text

```
Error: connect ECONNREFUSED 127.0.0.1:6379
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net) {
  errno: -111,
  code: 'ECONNREFUSED',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 6379
}
```

Most answers to this error tell you to start Redis. On your laptop that's correct, and it's why the advice is everywhere. On a server it misses the point, because the most useful part of the message is the address. `127.0.0.1` is the machine the app is running on. The app isn't failing to reach your Redis; it's trying to reach a Redis on its own machine, and there isn't one. Below is why that happens, the `::1` version of the same error, what it means when the host is right and the connection is still refused, and how to tell which case you have in about a minute.

## What ECONNREFUSED means

ECONNREFUSED is an operating system error, not a Redis or Postgres one. Your process sent a TCP connection request, a machine at that address answered, and the answer was a reset: nothing is listening on that port. The [Node.js documentation](https://nodejs.org/api/errors.html#common-system-errors) describes it as a connection that could not be made because the target machine actively refused it. Note what that rules out. The network path works and the host exists; only the port is empty. (A firewall configured to reject rather than drop can produce the same refusal, but that's rare on a hosting platform.)

That makes it different from the errors it gets confused with, and the difference tells you where to look:

| Error                                  | What happened                                                | Where to look                                                                     |
| -------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| ECONNREFUSED                           | The host answered: nothing listens on this port              | The address and port in the error. Is the service running there at all?           |
| ETIMEDOUT                              | No answer before the timeout                                 | Firewall, wrong private address, a network the app can't reach                    |
| ENOTFOUND                              | The hostname didn't resolve                                  | A typo in the host, or an internal hostname used from outside its network         |
| ECONNRESET                             | The connection was accepted, then dropped                    | Server restart, TLS mismatch, idle connection killed by a proxy                   |
| FATAL: sorry, too many clients already | Postgres accepted the TCP connection and refused the session | Connection limits. [A different problem](https://runsite.app/blog/postgres-too-many-clients-already) |

The same reset is behind every "connection refused" you have seen, from SSH and browsers too. When an app talks to its database, though, the address in the message usually gives the cause away.

Read the address before anything else

If the error says `127.0.0.1` or `::1` and your database lives somewhere else, stop debugging the database. The app is calling itself, and the fix is in its configuration.

## Why the address says 127.0.0.1

### The connection string never arrived

This is the most common cause after a first deploy. Redis and Postgres clients have defaults, and the defaults point at the local machine. `createClient()` in node-redis connects to `localhost:6379` when it gets no options, and `new Redis()` in ioredis falls back to port 6379 on the local machine. node-postgres falls back to `localhost:5432` when neither a connection string nor the `PGHOST` variable is set. So code like this works on a laptop where Redis is installed, and quietly falls back to the default on a server where `REDIS_URL` was never set:

javascript

```
// Works locally. On a server without REDIS_URL, url is undefined
// and the client silently connects to localhost:6379.
const redis = createClient({ url: process.env.REDIS_URL });
```

With ioredis the log often shows `[ioredis] Unhandled error event: Error: connect ECONNREFUSED 127.0.0.1:6379` repeating, because the client keeps retrying the wrong address. The variable is missing for ordinary reasons: it was in a local `.env` file that is not deployed, it was set on the old host and not the new one, or it was set on the web service and not on the worker. Moving off a platform that injected these values for you is a classic trigger. Heroku set `DATABASE_URL` and `REDIS_URL` automatically when you attached an add-on, and [the first deploy elsewhere is where you find out](https://runsite.app/blog/migrate-from-heroku) which ones your code relied on.

text

```
[ioredis] Unhandled error event: Error: connect ECONNREFUSED 127.0.0.1:6379
```

The fix is to stop the default from being reachable at all. Read the variable once, at startup, and refuse to boot without it:

javascript

```
function requireEnv(name) {
  const value = process.env[name];
  if (!value) {
    console.error(`${name} is not set; refusing to start`);
    process.exit(1);
  }
  return value;
}

const redis = createClient({ url: requireEnv("REDIS_URL") });
const pool = new Pool({ connectionString: requireEnv("DATABASE_URL") });
```

A crash at boot with a clear message is a better outcome than a running app that fails on its first cache call. The deploy fails, the log names the missing variable, and on a platform that keeps the old release until the new one is healthy, the old version goes on serving.

### localhost was hardcoded for your laptop

The second cause is the same thing written directly into code or config: `host: "localhost"` in a database config file, `redis://127.0.0.1:6379` as a fallback after `||`, a `config/production.json` copied from development. Search the repository for `localhost` and `127.0.0.1`. A fallback such as `process.env.REDIS_URL || "redis://localhost:6379"` is the most common version, and it causes the same quiet failure as no variable at all.

### Inside a container, localhost is the container

In Docker, each container has its own network namespace, so `localhost` inside the app container means the app container. A Redis running in another container, or on the host, is not there. This is why `connect ECONNREFUSED 127.0.0.1:5432` fills the Docker forums: Postgres runs fine, `psql` from the host connects fine, and the app in its container still can't reach it. In Docker Compose, services reach each other by service name:

yaml

```
services:
  app:
    build: .
    environment:
      REDIS_URL: redis://cache:6379        # the service name, not localhost
      DATABASE_URL: postgresql://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy   # wait until Postgres accepts connections
  cache:
    image: redis:7
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 2s
      retries: 15
```

If the database runs on your machine and only the app is containerised, `host.docker.internal` reaches the host on Docker Desktop. On Linux you need to add it yourself with `extra_hosts: ["host.docker.internal:host-gateway"]`. On a hosting platform the same principle applies with different names: the app has to use the hostname the platform gave the database, never a loopback address.

### ECONNREFUSED 127.0.0.1:5432: the same mistake with Postgres

Postgres shows the same mistake in its own words. From Node it looks like the Redis error with a different port; from libpq-based tools such as `psql` or psycopg2 it reads differently:

text

```
# Node (pg, Sequelize, TypeORM)
Error: connect ECONNREFUSED 127.0.0.1:5432

# libpq-based clients: psql, psycopg2, Ruby pg, PHP pgsql
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: Connection refused
	Is the server running on that host and accepting TCP/IP connections?
```

The diagnosis is identical. Check that `DATABASE_URL` is set in the environment the process actually runs in. Check also that it points at port 5432 or at a pooler port such as 6432 deliberately: if the string uses 6432 and no [connection pooler is listening there](https://runsite.app/blog/pgbouncer-connection-pooling), you get the same refusal with the right host.

## ECONNREFUSED ::1: when localhost stopped meaning 127.0.0.1

A variant that confuses people because nothing in their code changed:

text

```
Error: connect ECONNREFUSED ::1:6379

# libpq
connection to server at "localhost" (::1), port 5432 failed: Connection refused
```

`::1` is the IPv6 loopback address. Since Node.js 17, `dns.lookup` returns addresses in the order the resolver gives them (`verbatim` is true by default), and many systems return `::1` first for `localhost`, where older Node versions put IPv4 first. Redis and Postgres are often configured to listen on IPv4 only, so a connection to `localhost` that worked on Node 16 is refused on Node 17 to 19\. Node.js 20 and later turn on `autoSelectFamily` by default and fall back to the other address family, which hides the problem in many setups. Some clients resolve addresses themselves and skip that fallback, and on Node 20+ the same failure can still surface as an `AggregateError` with code `ECONNREFUSED`, holding one error per address tried.

Locally, the fix is to write `127.0.0.1` instead of `localhost`, to start Node with `--dns-result-order=ipv4first`, or to make the server listen on both addresses. In production it's the same fix as before. A deployed app shouldn't be connecting to loopback at all, so `::1` in a production log means the connection string is missing or hardcoded.

## The host is right and it's still refused

If the address in the error is the real database host, the app is configured correctly and the database side isn't listening at that moment. Four causes cover most cases.

* The app started before the database was ready. Docker Compose `depends_on` waits for the database container to start, not for the database inside it to accept connections. Postgres needs a few seconds to initialise on first run, and an app that connects during that window gets refused. Add a `healthcheck` to the database and use `depends_on` with `condition: service_healthy`, as described in Docker's [startup order documentation](https://docs.docker.com/compose/how-tos/startup-order/).
* The database restarted. A restart, failover or upgrade closes the port for a few seconds. Connections in flight drop with `ECONNRESET`, and new ones are refused until the server is back.
* The server only listens on loopback. Since Redis 6.2 the [default configuration](https://redis.io/docs/latest/operate/oss%5Fand%5Fstack/management/config/) ships with `bind 127.0.0.1 -::1`, and PostgreSQL defaults to [listen\_addresses = 'localhost'](https://www.postgresql.org/docs/current/runtime-config-connection.html). Installed from a distro package on a VM, either one refuses everyone who isn't on the same machine until you change that. The official Docker images already listen on all interfaces. Opening a server up means setting a password and TLS too.
* The port is wrong. A TLS port instead of the plain one, a pooler port with no pooler, or a port from an old connection string.

### Retry on startup instead of sleeping

The first two causes are temporary, and the right response is a short retry with backoff, not a `sleep 10` in the entrypoint that is too long on a fast day and too short on a slow one. Redis clients usually retry on their own: node-redis and ioredis keep reconnecting by default, so for them the job is to handle the `error` event and not crash on it. A Postgres pool tries once and throws. For that first query at startup, a small loop is enough:

javascript

```
async function connectWithRetry(connect, attempts = 6) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await connect();
    } catch (error) {
      if (error.code !== "ECONNREFUSED" || attempt === attempts) throw error;
      const delayMs = Math.min(500 * 2 ** attempt, 8000);
      console.warn(`database not ready (attempt ${attempt}), retrying in ${delayMs} ms`);
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }
}

await connectWithRetry(() => pool.query("SELECT 1"));
```

Keep the retry narrow. Retrying only on `ECONNREFUSED`, and only a few times, covers a database that is starting. A wrong address still fails after about 23 seconds of waiting (1, 2, 4, 8 and 8 seconds), with the real error in the log, instead of retrying forever.

## A 60-second diagnosis

Run these from the same environment the app runs in: a shell in the container, or the platform's console for the service. Running them from your laptop tests your laptop's network, which is a different question.

bash

```
# 1. Is the variable there, and what host does it name?
printenv | grep -E 'REDIS_URL|DATABASE_URL|PGHOST'

# 2. Does something answer on that host and port?
nc -zv cache.example.eu 6379

# 3. Does the service itself respond?
redis-cli -u "$REDIS_URL" ping          # PONG (rediss:// needs redis-cli 6.2+)
pg_isready -d "$DATABASE_URL"           # accepting connections

# What plain redis-cli prints when nothing listens locally:
# Could not connect to Redis at 127.0.0.1:6379: Connection refused
```

| What you see                                           | Cause                                              | Fix                                                     |
| ------------------------------------------------------ | -------------------------------------------------- | ------------------------------------------------------- |
| Variable empty, error says 127.0.0.1 or ::1            | Connection string never reached this environment   | Set it on this service; fail at boot when it is missing |
| Variable set, error still says localhost               | Hardcoded host or a \|| fallback in code           | Search the codebase for localhost and 127.0.0.1         |
| Works from the host, refused from the container        | localhost means the container                      | Use the service name or the platform's hostname         |
| Right host, refused only at startup or after a restart | Database not ready yet                             | Healthcheck plus a bounded retry with backoff           |
| Right host, always refused                             | Wrong port, or the server only listens on loopback | Check the port; set bind or listen\_addresses           |

One related case on deploys: the brief window where an old instance has stopped and the new one isn't listening yet also produces refused connections, for callers of your app this time rather than your app's calls to a database. Behind a reverse proxy, your visitors see that as a [502 Bad Gateway from nginx](https://runsite.app/blog/502-bad-gateway-nginx). That one belongs to how the platform stops containers, covered in [exit code 137 and SIGTERM handling](https://runsite.app/blog/exit-code-137).

## How Runsite handles it

On Runsite, [managed Redis comes with a ready-made connection string](https://runsite.app/services/redis) that you copy into the environment variables of every service that uses it, web and worker alike. Services inside Runsite reach the instance over private networking, and connections are encrypted with TLS by default, so the string that works in production is a real hostname, not a loopback address. [Managed PostgreSQL](https://runsite.app/services/postgresql) gives each database an internal hostname for your services and a separate external one for your laptop or a BI tool. If you see `127.0.0.1` in an error on Runsite, the variable is missing from that service or localhost is hardcoded in the code.

A failed Redis instance is restarted and rescheduled onto a healthy node, and connections drop while that happens, so the bounded retry above applies here as it does anywhere. Point the service's health check at an endpoint that touches the database rather than one that only proves the port is open, so a release that can't reach its data never takes traffic. The databases and services run in Germany.

## The short version

* ECONNREFUSED means a host answered and nothing listens on that port. The address in the error is the first thing to read.
* `127.0.0.1` or `::1` in a deployed app means it's calling its own machine: a missing `REDIS_URL` or `DATABASE_URL`, a hardcoded localhost, or a container where localhost is the container.
* Fail at boot when the connection string is missing, instead of letting the client fall back to its localhost default.
* `::1` after a Node upgrade is the IPv6 loopback; locally use `127.0.0.1`, in production fix the connection string.
* If the host is right, the database wasn't listening at that moment: use healthchecks and a short retry with backoff, then check the port and the server's listen address.

Related service

## Managed Redis on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed Redis](https://runsite.app/services/redis)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does ECONNREFUSED mean?

ECONNREFUSED is an operating system error meaning the connection was actively refused: your process reached the target host, and the host replied that nothing is listening on that port. The network path and the host are fine; the port is empty. When the address in the error is 127.0.0.1 or ::1, the app is trying to connect to its own machine, which in a deployed app usually means the database connection string is missing or hardcoded to localhost.

### How do I fix an ECONNREFUSED error?

Read the address and port in the error first. If it is 127.0.0.1 or ::1 and the database runs elsewhere, set REDIS\_URL or DATABASE\_URL in the environment where the app runs, remove any localhost fallbacks in code, and in Docker Compose use the service name instead of localhost. If the address is the real database host, the database was not listening at that moment: add a healthcheck so the app starts after the database is ready, retry the first connection with backoff, and check the port and the server's bind or listen\_addresses setting.

### What is port 6379 used for?

6379 is the default port of Redis. An error such as connect ECONNREFUSED 127.0.0.1:6379 means a Redis client tried the default local address and found no Redis there. Locally, that means Redis isn't running. On a server, it usually means the client got no connection URL and fell back to its default of localhost:6379, so the fix is to pass the real Redis URL through an environment variable.

### How do I fix "connection to server at localhost (::1), port 5432 failed: Connection refused"?

::1 is the IPv6 loopback address. The client resolved localhost to IPv6, and the connection failed. psql tries every address localhost resolves to, so this usually means PostgreSQL is not running, or localhost maps only to ::1 while the server listens on IPv4\. Node.js 17 to 19 try only the first address, so there an IPv4-only server is enough to fail. Locally, connect to 127.0.0.1 instead of localhost, or set listen\_addresses in postgresql.conf to include both addresses and restart PostgreSQL. In a deployed app, a localhost address means the connection string is not set, so set DATABASE\_URL to the real database host.

Keep reading

---

# 502 Bad Gateway in Nginx After Deploy: Read the Error Log

> 502 Bad Gateway means nginx got no valid response from your app. Match the error.log line (111 refused, 104 reset, closed early) to the cause and fix.

1. [Home](https://runsite.app/)
2. [Blog](https://runsite.app/blog)
3. 502 Bad Gateway in Nginx After Deploy: Read the Error Log

DeploymentOctober 9, 20269 min read

# 502 Bad Gateway in Nginx After Deploy: Read the Error Log

A 502 Bad Gateway page from nginx says nothing about the cause. The matching line in error.log does: connection refused, a worker that died mid-response, a keep-alive reset or an oversized header. What each one means and how to fix it after a deploy.

## The short version

A 502 Bad Gateway from nginx means nginx reached for your application and got either no response or an unusable one. The error page never says which. The line nginx wrote to error.log at the same moment does: connect() failed (111: Connection refused) means nothing was listening, upstream prematurely closed connection means the app died or hung up mid-request, recv() failed (104: Connection reset by peer) often means a keep-alive timeout mismatch or an app restart, and upstream sent too big header means the response headers outgrew the proxy buffer. After a deploy, the usual cause is the first one: the app listens on the wrong port or on 127.0.0.1, or traffic arrived before it was listening.

[Teo Marquardt](https://runsite.app/about#author)· Facts last checked October 9, 2026

You deployed, opened the site, and got a white page with two lines on it:

html

```
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx</center>
</body>
</html>
```

The page is generated by nginx, not by your application, and it is identical whatever went wrong. That's why most advice for this error is a list of guesses, usually starting with "restart it" and ending with "raise the timeout". You don't have to guess. Every time nginx returns a 502, it writes one line to its error log saying why, and there are only a handful of possible lines. Read that line first and the list of suspects drops to one. Below are the lines you will actually see, what each means, the extra causes that show up right after a deploy, and how to tell 502 apart from 503 and 504.

## What 502 Bad Gateway means

A request to a site behind nginx travels in two hops: browser to nginx, then nginx to your application, which nginx calls the upstream. [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-502-bad-gateway) defines 502 as a gateway or proxy that "received an invalid response from an inbound server". In practice nginx also uses it when there was no response at all: the connection was refused, or it was dropped before a complete response header arrived. Either way, the first hop worked. nginx is up and answering. The second hop failed, and nearly always the cause is on your application's side of it.

### 502 vs 503 vs 504

The three 5xx codes a proxy returns are easy to mix up, and the difference narrows the search before you open any log:

| Code                    | What the proxy is telling you                                | Typical cause                                                                                                                   |
| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| 502 Bad Gateway         | I reached for the upstream and got nothing usable            | App not listening, crashed mid-request, connection reset, malformed or oversized response headers                               |
| 503 Service Unavailable | I won't or can't pass this on right now                      | Maintenance mode, overload, or a rate or connection limit at the proxy (nginx limit\_req and limit\_conn return 503 by default) |
| 504 Gateway Timeout     | The upstream accepted the request and never answered in time | A slow query or blocked event loop that outlasts proxy\_read\_timeout (60 seconds by default)                                   |

A 504 is a slow app; a 502 is an absent or broken one. That one distinction already rules out the timeout settings that half the advice on 502 tells you to raise. Raising `proxy_read_timeout` fixes 504s, and does nothing for a 502.

## Find the line in error.log first

On a server with a distribution package, the log is at `/var/log/nginx/error.log`. With the official nginx Docker image it goes to the container's standard error, so `docker logs <nginx-container>` shows it. Find the entry with the same timestamp as your failed request. It looks like this:

text

```
2026/10/09 09:14:02 [error] 31#31: *4521 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.7, server: app.example.eu, request: "GET / HTTP/1.1", upstream: "http://10.0.1.12:3000/", host: "app.example.eu"
```

Two parts matter. The phrase in the middle is the reason, and the `upstream:` field is the exact address and port nginx tried. Here is what each reason means:

| Line in error.log                                                                         | What happened                                                                                                                                        | Where to look                                                |
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| connect() failed (111: Connection refused) while connecting to upstream                   | Nothing was listening at the upstream address                                                                                                        | Is the app running? Right port? Listening on 0.0.0.0?        |
| upstream prematurely closed connection while reading response header from upstream        | The app accepted the request, then closed the connection without answering                                                                           | App logs at the same second: crash, OOM kill, worker timeout |
| recv() failed (104: Connection reset by peer) while reading response header from upstream | The app reset the connection                                                                                                                         | Keep-alive timeouts between nginx and the app; app restarts  |
| upstream sent too big header while reading response header from upstream                  | The response headers did not fit in nginx's buffer                                                                                                   | Large cookies or headers; proxy\_buffer\_size                |
| no live upstreams while connecting to upstream                                            | Every server in a multi-server upstream group is marked unavailable, after max\_fails failures (default 1) within fail\_timeout (default 10 seconds) | The errors that came before this one in the log              |

Log first, then theories

If you can't see the nginx error log, the most useful thing to do is get access to it. Every fix below depends on knowing which of these lines you have, and they have nothing in common except the status code.

## connect() failed (111: Connection refused) while connecting to upstream

Errno 111 is `ECONNREFUSED`: nginx reached the address in the `upstream:` field and the operating system there replied that nothing listens on that port. It's the same refusal your own code sees when [your app can't reach its database](https://runsite.app/blog/econnrefused-127-0-0-1), only now your app is the one not answering. Four causes cover almost every case.

### The app is not running

It crashed at startup, it's in a restart loop, or it never started because the build output is missing. Check the application's own log and its process or container status. A crash loop produces 502s in bursts that line up with each restart.

### The app listens on a different port

nginx is pointed at 3000 and the app started on 8080, or the app reads `PORT` from the environment and the variable holds a different value than the proxy config expects. Compare the port in the `upstream:` field with the port the app reports at startup. If the app prints nothing at startup, make it print its address. One log line, and next time this check takes five seconds.

### The app listens on 127.0.0.1 inside its container

This is the one that fools people, because the app is running and the port is right. Many development servers bind to loopback by default: `flask run`, Django's `runserver`, `rails server` in development and Vite's dev server all listen on loopback (`127.0.0.1`, or `::1` in some Vite and Node setups) unless told otherwise. Inside a container or on a separate VM, loopback is reachable only from that same machine, so a proxy anywhere else gets refused. Check from inside the app's environment:

bash

```
# What is listening, and on which address?
ss -ltnp
# LISTEN 0  511  127.0.0.1:3000  0.0.0.0:*   <- only reachable from inside
# LISTEN 0  511    0.0.0.0:3000  0.0.0.0:*   <- reachable by the proxy

# Does the app answer locally at all?
curl -i http://127.0.0.1:3000/
```

If `curl` works from inside and nginx still gets refused, the bind address is the problem. Start the server on `0.0.0.0` (for example `flask run --host=0.0.0.0` or `gunicorn --bind 0.0.0.0:8000`) and run a production server rather than the development one while you're at it.

### proxy\_pass points at localhost inside a container

The mirror image of the previous cause. When nginx itself runs in a container, `localhost` in its config means the nginx container, not the app container next to it. In Docker Compose, use the service name:

nginx

```
location / {
    # proxy_pass http://localhost:3000;   # 502: localhost is the nginx container
    proxy_pass http://app:3000;           # "app" is the Compose service name
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}
```

On a PHP stack the same failure names a socket instead of a port: `connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)` means PHP-FPM is not running or uses a different socket path, and `(13: Permission denied)` means nginx's user can't open the socket file.

## upstream prematurely closed connection while reading response header from upstream

This time the connection worked. nginx sent the request, the app accepted it, and then the connection closed before a response header arrived. Something on the app side ended the request without answering. Look at the application log at the same second, and you'll usually find one of these:

* The process was killed. A container that hits its memory limit is killed instantly, mid-request, and the requests it was serving end exactly like this. If the container's exit code is 137, [the kernel or the orchestrator sent SIGKILL](https://runsite.app/blog/exit-code-137).
* A worker timed out. gunicorn kills a sync worker that takes longer than `--timeout`, which is 30 seconds by default, and the request it was handling gets no response. nginx would have waited 60 seconds, so the 502 arrives before nginx's own timeout and looks like a crash.
* An unhandled exception took the process down. In Node, an uncaught error in a request handler can exit the whole process, and every request in flight gets the same 502.
* The app closed an idle keep-alive connection just as nginx reused it. It's the same race as in the next section: a clean close produces this line, a reset produces the 104 one.

The fix depends on which one it was. An OOM kill needs more memory or less of it per request. A worker timeout needs a longer `--timeout`, or better, a background job for the slow work. A crash needs an error handler that returns a 500 instead of taking the process down with it.

## recv() failed (104: Connection reset by peer)

Errno 104 is `ECONNRESET`. The full line usually ends with `while reading response header from upstream`. Apart from an app that restarts, a common cause is a keep-alive timeout mismatch. nginx does not reuse upstream connections by default. It starts once you set `keepalive` in an `upstream` block together with `proxy_http_version 1.1;` and `proxy_set_header Connection "";`, and many load balancers pool connections on their own. In that setup each side has its own idle timeout. If the app closes an idle connection at the same moment the proxy sends a new request down it, the request lands on a closed socket and fails. The result is rare, random 502s under light load that no one can reproduce.

Node.js makes this easy to hit: as of writing, `server.keepAliveTimeout` defaults to 5 seconds, while nginx, once upstream keepalive is on, keeps idle connections for 60 seconds by default (`keepalive_timeout` in the upstream module) and many cloud load balancers use 60 seconds or more. The rule is that the app should hold an idle connection longer than the proxy does, so the proxy is always the side that closes it:

javascript

```
const server = app.listen(port, "0.0.0.0");

// Keep idle connections open longer than the proxy in front does,
// so the proxy always closes first and never reuses a dead socket.
server.keepAliveTimeout = 65_000;
// Must be larger than keepAliveTimeout.
server.headersTimeout = 66_000;
```

Other servers have the same setting under another name, for example gunicorn's `--keep-alive` or uvicorn's `--timeout-keep-alive`. The values matter less than the order: the app's idle timeout must be longer than the proxy's.

## upstream sent too big header while reading response header from upstream

nginx reads the response headers into a buffer whose size is `proxy_buffer_size`, one memory page by default (4 KB or 8 KB depending on the platform). A response with large `Set-Cookie` headers, for example a session stored in a cookie or an authentication library that sets several large cookies at once, can overflow it. The request works for some users and fails with 502 for others, usually the ones with the most cookies. Shrinking the headers is the proper fix. Raising the buffer gets you through the afternoon:

nginx

```
proxy_buffer_size 16k;
proxy_buffers 8 16k;
```

This is the 5xx cousin of [413 Request Entity Too Large](https://runsite.app/blog/413-request-entity-too-large): there nginx rejects a request body that is too big, here it rejects response headers that are too big.

## 502 right after a deploy

If the 502s started with a release, the log line will usually be the refused connection, and the cause is one of a short list:

* The port moved. The new build listens on a different port than the old one, or the code ignores the port the platform tells it to use (often through a `PORT` variable) in favour of a hardcoded number.
* The new build binds 127.0.0.1\. A changed start command, a framework upgrade, or a switch to the development server.
* Traffic arrived before the app was listening. The proxy sent requests to the new instance during its startup, while it was still running migrations, warming a cache or connecting to the database. This lasts seconds and then fixes itself, which is why it's easy to dismiss.
* The old instance was stopped with requests in flight. If the old process exits on SIGTERM without finishing what it was serving, those requests end as `upstream prematurely closed connection`. A [rolling or blue-green deploy](https://runsite.app/blog/blue-green-vs-rolling-deployments) only helps if the old instance finishes its in-flight work before it exits.

The first two are configuration and stay broken until fixed. The last two are timing, and the fix is a health check the platform waits on before sending traffic, plus a graceful shutdown. A minimal server that gets all four right:

javascript

```
// Use the port from your service settings; PORT is a common convention.
const port = Number(process.env.PORT ?? 3000);

// Healthy only when the app can actually serve: it touches the database.
app.get("/healthz", async (req, res) => {
  try {
    await pool.query("SELECT 1");
    res.sendStatus(200);
  } catch {
    res.sendStatus(503);
  }
});

const server = app.listen(port, "0.0.0.0", () => {
  console.log(`listening on 0.0.0.0:${port}`);
});

// On SIGTERM stop accepting new connections, finish the ones in flight, then exit.
process.on("SIGTERM", () => {
  server.close(() => process.exit(0));
  server.closeIdleConnections?.(); // Node 18.2+: drop idle keep-alive sockets too
});
```

Note what the health check does. An endpoint that returns 200 without touching anything only proves the port is open, so a release that can't reach its database passes the check, takes traffic and fails every request. A check that runs `SELECT 1` keeps that release out of rotation.

## How Runsite handles it

[Runsite web services deploy with rolling releases gated by a health check](https://runsite.app/services/web-services). New instances start alongside the ones already serving, traffic moves to them only after the health check passes, and a release that fails the check is rolled back automatically, so the old version keeps answering. That keeps the "traffic arrived before the app was listening" case away from users, as long as the health check proves the app can actually serve. The health check path and interval are configurable, which is how you point it at an endpoint like the `/healthz` above. Containers that fail are restarted and rescheduled onto healthy nodes, and the services run on servers in Germany.

What the platform can't fix for you is the app itself: it still has to listen on `0.0.0.0` and on the port the platform sends traffic to, and handle SIGTERM by finishing its in-flight requests. Service settings are described in the [Runsite docs](https://docs.runsite.app).

## The short version

* A 502 means nginx reached for your app and got no usable response. The error page is the same every time; the line in error.log is not.
* `connect() failed (111: Connection refused)`: nothing listens at the `upstream:` address. Check that the app runs, the port matches, and it binds 0.0.0.0, not 127.0.0.1.
* `upstream prematurely closed connection`: the app died or hung up mid-request. Look for an OOM kill, a worker timeout or an uncaught exception at the same second.
* `recv() failed (104: Connection reset by peer)`: often a keep-alive mismatch or a restarting app. If the proxy reuses connections, make the app's idle timeout longer than the proxy's; otherwise look for a restarting app.
* 502s right after a deploy are a wrong port, a loopback bind, or traffic reaching an instance before it's ready. A health check that touches the database plus a graceful shutdown fixes the timing cases.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](https://runsite.app/services/web-services)

[Back to all articles](https://runsite.app/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does the 502 Bad Gateway error mean in Nginx?

It means nginx, acting as a reverse proxy, tried to pass the request to your application (the upstream) and got no valid response: the connection was refused, closed or reset before a complete response header arrived, or the headers were too large for nginx's buffer. nginx itself is working. The exact reason is in the nginx error log, in a line such as connect() failed (111: Connection refused) while connecting to upstream.

### Is a 502 Bad Gateway my fault?

If you are visiting someone else's site, no. The problem is between that site's proxy and its application, and the only thing to do is wait or try again later. If it is your site, the cause is almost always your application or its configuration rather than nginx: the app is not running, listens on the wrong port or address, crashed during the request, or closes keep-alive connections earlier than the proxy expects.

### How to fix 502 Bad Gateway on nginx?

Open the nginx error log (/var/log/nginx/error.log, or docker logs for the nginx container) and find the line logged with the failed request. For connect() failed (111: Connection refused), make sure the app is running, listens on the port in the upstream address, and binds 0.0.0.0\. For upstream prematurely closed connection, check the app log for crashes, out-of-memory kills or worker timeouts. For recv() failed (104: Connection reset by peer), raise the app's keep-alive timeout above the proxy's. For upstream sent too big header, reduce cookie size or raise proxy\_buffer\_size.

### What is the difference between 502 and 504?

A 502 Bad Gateway means the proxy got no usable response from the upstream: nothing was listening, or the connection broke before the response header arrived. A 504 Gateway Timeout means the upstream accepted the request but did not respond within the proxy's timeout, which in nginx is usually proxy\_read\_timeout (60 seconds by default); proxy\_connect\_timeout, also 60 seconds, produces a 504 when the upstream address does not answer at all. A 502 points at an absent or crashing app; a 504 points at a slow one, and only the 504 is fixed by changing timeouts.

Keep reading

---

# Pricing — Hourly Billing, No Seat Fees

> Hourly pricing in euro. Nano web services and PostgreSQL from €3/mo, Redis from €5/mo. €5 of credit on every new account, no card required. EU hosting.

Hourly billing in euro

# Simple, transparent pricing

Pay only for the resources your app actually uses. Switch between monthly and hourly to see exactly what you'll spend. No per-seat fees, no egress charges, no surprise bills.

[Claim €5 credit](https://dashboard.runsite.app/login)[Try the calculator](https://runsite.app/pricing/calculator)[Compare plans](#plans)

Runsite is in Public Beta

Every service listed here is live, and paid plans bill exactly as shown. Beta means the platform is young. Features still change, and parts of the roadmap are not built yet.

There is no separate SLA during the beta. The uptime commitments that apply are in [section 8 of the Terms of Service](https://runsite.app/terms#sla). We will publish an SLA with service credits before the beta ends.

Hourly billing

Pay only for the hours your service runs

Hard spending limit

Cap your monthly bill, no surprises

Unlimited team

Zero per-seat fees, ever

EU-sovereign

GDPR-ready, signed DPA included

## €5of credit on every new account

Enough to run something real, not a sandbox. No credit card to start, and static sites are never billed at all.

[Claim €5 credit](https://dashboard.runsite.app/login)

How far €5 goes

* Nano web service30 days

0.1 vCPU · 256 MB
* Web service + Nano PostgreSQL\~25 days

0.1 vCPU · 256 MB + 1 GB

Hourly billing, so the credit is spent only while services are running.

* No credit card to start
* One service of each type on Nano or Starter until you top up
* Credit gone? Services stop, data stays. Top up to bring them back

### Long-running web servers

·Apps · APIs · Backends

Push your repo, Runsite builds and serves. Auto-scale, zero-downtime rolling deploys, free SSL.

Web Services

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Disk

1 GB

Bandwidth

100 GB

[Choose Nano](https://dashboard.runsite.app/login)

Web Services

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

512 MB

Disk

5 GB

Bandwidth

1 TB

[Choose Starter](https://dashboard.runsite.app/login)

Web Services

### Standard

Popular

€12/mo

≈ €0.0164/hour, billed hourly

vCPU

1

RAM

1 GB

Disk

10 GB

Bandwidth

2 TB

[Choose Standard](https://dashboard.runsite.app/login)

Web Services

### Pro

€25/mo

≈ €0.0342/hour, billed hourly

vCPU

1

RAM

2 GB

Disk

10 GB

Bandwidth

5 TB

[Choose Pro](https://dashboard.runsite.app/login)

Web Services

### Pro Plus

€50/mo

≈ €0.0685/hour, billed hourly

vCPU

2

RAM

4 GB

Disk

20 GB

Bandwidth

10 TB

[Choose Pro Plus](https://dashboard.runsite.app/login)

Web Services

### Business

€95/mo

≈ €0.1301/hour, billed hourly

vCPU

4

RAM

8 GB

Disk

20 GB

Bandwidth

20 TB

[Choose Business](https://dashboard.runsite.app/login)

Web Services

### Business Plus

€180/mo

≈ €0.2466/hour, billed hourly

vCPU

8

RAM

16 GB

Disk

30 GB

Bandwidth

30 TB

[Choose Business Plus](https://dashboard.runsite.app/login)

Nano is the entry plan: the same 0.1 vCPU / 256 MB as before, now €3/mo and covered by your €5 starting credit. Nano services draw on a 100 GB monthly egress pool, ship a noindex header, and sleep after 14 days with no traffic. Starter and above are always-on, indexable and unmetered on egress.

### JAMstack & static frontends

·Landing pages · Docs · Portfolios

Deploy Next.js, Astro, Vite, Hugo and friends to a global CDN. Auto-deploy from Git, preview on every PR, free SSL.

Static Sites

### Free

Popular

Free

Never billed · never draws on your balance

CDN

Global

Bandwidth

100 GB

Builds

\~20s

Sites

Unlimited

[Deploy for free](https://dashboard.runsite.app/login)

Static sites are not billed. No plan, no hourly charge, and they never draw on your balance or credit. Pay-as-you-go bandwidth above the included quota will be introduced after launch. We'll notify you before any change.

### Production-ready databases

·Managed · Backups · EU-sovereign

One-click PostgreSQL with daily backups, connection pooling, encrypted at rest, never leaves the EU.

PostgreSQL

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Storage

1 GB

Connections

25

[Choose Nano](https://dashboard.runsite.app/login)

PostgreSQL

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

1 GB

Storage

5 GB

Connections

50

[Choose Starter](https://dashboard.runsite.app/login)

PostgreSQL

### Standard

Popular

€14/mo

≈ €0.0192/hour, billed hourly

vCPU

1

RAM

2 GB

Storage

10 GB

Connections

100

[Choose Standard](https://dashboard.runsite.app/login)

PostgreSQL

### Pro

€35/mo

≈ €0.0479/hour, billed hourly

vCPU

2

RAM

4 GB

Storage

20 GB

Connections

200

[Choose Pro](https://dashboard.runsite.app/login)

PostgreSQL

### Pro Plus

€70/mo

≈ €0.0959/hour, billed hourly

vCPU

4

RAM

8 GB

Storage

50 GB

Connections

400

[Choose Pro Plus](https://dashboard.runsite.app/login)

PostgreSQL

### Business

€130/mo

≈ €0.1781/hour, billed hourly

vCPU

8

RAM

16 GB

Storage

100 GB

Connections

500

[Choose Business](https://dashboard.runsite.app/login)

Storage beyond the included quota: +€0.25/GB/mo. Daily encrypted backups included. Nano is the entry plan at €3/mo. There is no longer a 30-day free database. If your credit and balance run out the database stops with its volume intact; press Start after topping up and it comes straight back. Storage is reclaimed 7 days later and the final backup is kept for 12 months.

### Sub-ms in-memory store

·Cache · Sessions · Queues

Drop-in Redis 7+ compatible store. Pub/Sub, automatic failover, TLS, RDB + AOF persistence.

Redis-compatible

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

Memory

256 MB

Connections

250

[Choose Starter](https://dashboard.runsite.app/login)

Redis-compatible

### Standard

Popular

€13/mo

≈ €0.0178/hour, billed hourly

Memory

1 GB

Connections

1,000

[Choose Standard](https://dashboard.runsite.app/login)

Redis-compatible

### Plus

€25/mo

≈ €0.0342/hour, billed hourly

Memory

2 GB

Connections

2,000

[Choose Plus](https://dashboard.runsite.app/login)

Redis-compatible

### Pro

€65/mo

≈ €0.0890/hour, billed hourly

Memory

5 GB

Connections

5,000

[Choose Pro](https://dashboard.runsite.app/login)

Redis-compatible

### Max

€125/mo

≈ €0.1712/hour, billed hourly

Memory

10 GB

Connections

10,000

[Choose Max](https://dashboard.runsite.app/login)

Redis starts at Starter. The 50 MB free instance is no longer offered. Every plan here includes RDB + AOF persistence, TLS and automatic failover, and is billed by the hour like everything else. A Starter instance runs for roughly a month on the €5 starting credit, if it is the only thing you have running.

Runsite hosts applications, not relays. VPN endpoints, proxies, VLESS / Xray / V2Ray and Shadowsocks nodes, and traffic tunnels of any kind are prohibited on every plan, paid ones included, and are blocked without notice or refund. See the Acceptable Use Policy in Section 4 of the Terms.

Pay only for what you use

## No subscription. No minimum.

Storage and email are billed by actual consumption. Stop using them and the invoice for that service is exactly **€0**, even if everything else stays running.

### S3 Object Storage

User uploads, backups, assets

Pay-as-you-go

€0.025

per GB · per month

5 GB included

Included on every account, no card required

Usage examples/ month

* 5 GBIncluded
* 100 GB€2.50
* 500 GB€12.50
* 1 TB€25
* 5 TB€125

Zero egress fees

S3-compatible API

Public + private buckets

AES-256 encryption

Billed per GB-hour. Stop using it, stop paying. No minimums, no commitments.

### Transactional Email

Receipts, password resets, alerts

Pay-as-you-go

€1

per 1,000 emails

3,000 emails / month included

Production sending unlocks after a €5 top-up

Usage examples/ month

* 3,000Included
* 5,000€2
* 10,000€7
* 25,000€22
* 50,000€47

High-deliverability EU sending

HTML + text templates

Webhooks for opens & clicks

Custom sender domains

Charged only for the emails above your included quota. Sending to external recipients requires a minimum €5 top-up; before that, mail stays inside the sandbox. No subscription, no monthly minimum.

## Every plan includes

The boring-but-essential stuff, already in the box on every tier.

SSL certificates

Auto-deploy from Git

Unlimited team members

Custom domains

Real-time logs

Rollbacks

### Set a limit. Never overspend.

Tell us your max budget. When you hit the limit, services pause, not your wallet. No surprise bills.

### Unlimited team, zero seat fees

Add 2 teammates or 200 for the same price. Pay for compute, not collaboration.

Typical side project: 1 web service + 1 database

€6/month

Nano web (€3) + Nano Postgres (€3). Your €5 starting credit covers most of the first month. Whole team included, no per-seat fees.

FAQ · 06

## Pricing questions Clear answers.

Everything about billing, the starting credit, spending limits and refunds.

viewing01 / 06

### Still curious?

Real humans, real answers, usually within a few hours.

[support@runsite.app](mailto:support@runsite.app)

### How does the €5 starting credit work?

Every new account gets €5 of credit, valid for 30 days. No credit card, no card check, nothing to claim: it is on the balance the moment you sign up.

Charges are drawn from the credit first and from your real balance only after it is gone, and the two are always shown as separate numbers on the billing page. The credit is not a top-up: it never counts toward your lifetime spend, so it does not unlock the tiers or the production email sending that a real payment does.

What it buys: the entry Nano plans are €3/mo each. One Nano web service on its own runs the full 30 days and leaves credit unspent. A web service plus a Nano PostgreSQL costs €6 a month, so the pair runs about 25 days before the credit is gone. Top up any amount from €5 to carry it past that. Static sites are not billed at all and never touch the credit.

Two limits apply while you are running on credit alone. You get one service of each type (one web service, one PostgreSQL, one Redis, one cron job), and static sites are not counted against that. And you can only pick the entry plans: Nano, or Starter for the types that have no Nano. Standard and above need a real payment first, because €5 should not buy 8 vCPU for an afternoon. A single top-up of €5 lifts both.

When the credit runs out, or when the 30 days are up (whichever comes first), running services stop and the remainder is written off. Your real balance is never touched. Full breakdown on the pricing page.

### Do apps sleep or cold-start?

No cold starts while you're using the app. Runsite keeps containers warm as long as they receive traffic.

On Nano, the entry plan, a web service that gets zero requests for 14 consecutive days is suspended to free up shared capacity. The next incoming request wakes it back up (usually within a few seconds). Your code, environment variables, and storage are preserved.

In practice this only affects truly abandoned projects. Any live demo, bot, or side project with even occasional visitors will never sleep. Starter and above (from €5/mo) skip the inactivity sleep entirely and stay always-on with dedicated CPU.

### What happens when my credit runs out?

Your services stop. Nothing is deleted on the spot, and nothing is charged to a card you never gave us.

We email you on day 23 and day 29 of the credit, and again when services actually stop, so the wall is never a surprise. Top up any amount from €5 and press Start: a web service redeploys, a PostgreSQL database comes back with its volume exactly as it was.

Storage is held for 7 days after the stop. After that the PostgreSQL volume is reclaimed and the final backup is kept for 12 months, so a project you come back to months later can still be restored by topping up. Redis is in-memory and is not backed up, so export anything you need before the deadline.

Static sites are unaffected. They are never billed and never stop.

### How do spending limits work?

You set a monthly budget in the dashboard, say €20\. Services consume resources, we track usage against that cap in real time.

Hit 80% and you get an email. Hit 100% and services pause automatically. They stop running but aren't deleted, and nothing more gets charged. Raise the cap or wait for the next cycle to resume.

The cap is a hard stop, not a friendly email after the damage is done.

### Are there per-seat or per-user fees?

No. No per-seat, per-user, or per-team charges. Your whole team, 2 people or 200, can deploy, manage services, and read logs at no extra cost.

You pay for the infrastructure your services actually use: CPU, memory, storage. That's a different model from Heroku or Railway, where per-seat fees snowball as teams grow.

Shared dashboards, deploy permissions, and audit logs come with every plan.

### Can I run a VPN, proxy, or tunnel on Runsite?

No. Proxies, VPN endpoints, VLESS/Xray/V2Ray and Shadowsocks nodes, and traffic tunnels of any kind are prohibited on every plan, paid ones included. Services identified as running them are blocked without notice and without a refund.

This is not a bandwidth policy we might relax later. Relay traffic is what a shared platform cannot absorb: it saturates egress that other customers paid for, and it puts our IP ranges on blocklists that take months to leave.

Runsite is built for applications: web services, APIs, background jobs, and the databases behind them. If your workload is a legitimate app that happens to open outbound connections (a scraper, a bot, a webhook relay), that is fine. Anything whose purpose is to forward third-party traffic is not. Section 4 of the Terms of Service sets this out in full.

---

# Pricing calculator

> Pick your services and instantly see what Runsite costs per hour and per month. €5 of starting credit on every account. No credit card required.

[Back to pricing](https://runsite.app/pricing)

Live estimate

# Build your stack. See the bill update live.

Pick the services you actually need, set the size, and the calculator reveals your monthly and hourly cost as you go. No tricks, same rates we charge in the dashboard.

[Claim €5 credit](https://dashboard.runsite.app/login)[See full plans](https://runsite.app/pricing)

Hourly billing

Spin services up and down — pay only for the hours they run

€5 starting credit

Plus 5 GB S3 and 3,000 emails included every month

Quick start

Try a typical setup, then tweak from there

### Web Services

Apps, APIs, backends — billed by the hour

€12.00

per month

€12.00/mo

Plan

Instances

### PostgreSQL

Managed Postgres with daily backups

€14.00

per month

€14.00/mo

Plan

Databases

Extra storage

GB

€0.25 per extra GB · in addition to plan storage

Currently 0 GB

### Redis

Cache, sessions, queues — sub-ms latency

€13.00

per month

€13.00/mo

Plan

Instances

### S3 Object Storage

User uploads, backups, assets — €0.025/GB

€1.13

per month

€1.13/mo

Storage

GB

First 5 GB included · only billed beyond that

5 GB included

Currently 50 GB

### Email API

Transactional sending — €1 per 1k emails

€1.50

per month

€1.50/mo

Emails per month

First 3,000 emails included each month · production sending needs a €5 top-up

3k included

Heads up: compute services bill by the hour — you only pay for the time the service is actually running. S3 and Email are pay-as-you-go and reset their included quotas each calendar month. Every new account starts with €5 of credit, valid for 30 days.

Estimate

€41.63/mo

≈ €0.0570 /hour≈ €499.50 /year

Included quotas save you €3.13/mo

Breakdown

* PostgreSQL  
€14.00  
Standard · 1 vCPU · 2 GB · 10 GB disk
* Redis  
€13.00  
Standard · 1 GB
* Web Service  
€12.00  
Standard · 1 vCPU · 1 GB · 10 GB disk
* Email API  
€1.50  
4,500 emails · 1,500 billable @ €1/1k
* S3 Object Storage  
€1.13  
50 GB · 45 GB billable @ €0.025/GB

[Claim €5 credit](https://dashboard.runsite.app/login)

€5 credit on signup · no card · spend cap built in

---

# Web Services — Deploy Web Apps in Any Language

> Run Node.js, Python, Go, Ruby, or Rust apps on Runsite. Automatic builds, zero-downtime releases, EU-hosted infrastructure, from €3/mo with no cold starts.

1. [Home](https://runsite.app/)
2. [Services](https://runsite.app/services)
3. Web Services

Compute EU · Germany

# Deploy Web Apps in Any Language, Hosted in the EU

Node.js, Python, Go, Ruby, Rust, or your own Dockerfile — Runsite builds it, runs it, and keeps it online on European infrastructure.

[Start with €5 credit](https://dashboard.runsite.app/login)[Read the docs](https://docs.runsite.app/services/web-services/)

CPU

Up to 8 vCPU

Memory

Up to 16 GB RAM

Bandwidth

Up to 30 TB/mo

bash — deploy

 build 42

 git push origin main

 detected Node.js 22 · no Dockerfile needed

 building image ................ 18.2s

 health check /healthz ......... 200 OK

 releasing v42 · zero downtime

 live in 31s

 https://api-7f3.runsite.app 

Containers, env vars and logs stay in Germany

Runtimes

Node.js · Python · Go · Ruby · Rust · Docker · Next.js · Django · Rails · FastAPI

Overview

Runsite Web Services runs your web apps and APIs in production straight from Git. Connect a repository and every push to your main branch gets built into a container and released with zero downtime. You don't need a Dockerfile, since Runsite detects your framework, but you can supply your own when you want full control over the build.

Everything runs on infrastructure inside the European Union. Your code, environment variables, and runtime data stay on EU soil, and a signed GDPR Data Processing Agreement comes with every plan instead of an enterprise add-on.

It handles full-stack apps, REST and GraphQL APIs, and microservices in any language that runs in a container: Node.js, Python, Go, Ruby, Rust, and the rest. Reach for [managed PostgreSQL](https://runsite.app/services/postgresql) when you need a database, [Redis](https://runsite.app/services/redis) for caching, or [transactional email](https://runsite.app/services/email) for the mail your app sends. If you are shipping a prebuilt site rather than a server, [Static Sites](https://runsite.app/services/static-sites) is the lighter option — those are never billed. The entry Nano plan is €3/mo and covered by the €5 credit every new account starts with, and it keeps active apps warm with no cold starts.

Last reviewed: September 2026

At a glance

CPU

Up to 8 vCPU

Memory

Up to 16 GB RAM

Bandwidth

Up to 30 TB/mo

Deploy Time

\~30 seconds

Region

EU · Germany

Hosted in the EU · Germany

[Start with €5 credit](https://dashboard.runsite.app/login)

Features

## Everything to Ship Web Apps

Built-in tools and capabilities to deploy and manage with confidence.

### Auto-Deploy from Git

Push to your main branch and Runsite builds and deploys automatically. Supports GitHub, GitLab, and Bitbucket with webhook-driven pipelines.

The build runs on the commit, not on your machine, so what ships is reproducible and the log is there to read afterwards. Configuration is applied at release rather than baked into the image, which is what lets the same artifact run in a preview environment and in production.

### Preview Environments

Every pull request gets its own isolated environment with a unique URL. Review changes before merging with zero configuration.

The preview gets its own URL and its own process, so a reviewer clicks a link instead of checking out a branch. What it does not get is its own database unless you attach one, which is worth knowing before a reviewer writes test data into production rows.

### Zero-Downtime Deploys

Rolling deployments ensure your application is always available during updates. Automatic rollback on health check failures.

Instances are replaced in waves with a health check between each one, which means the old and new versions serve traffic at the same time for a minute or two. Every migration therefore has to be backward compatible with the version it is replacing, or the overlap breaks it.

### Health Checks & Auto-Restart

Configure custom health check paths and intervals. Failed containers are automatically restarted and rescheduled on healthy nodes.

A check that only proves the port is open will pass while the database connection is down, so point it at something that exercises a dependency. Interval and timeout decide how long a genuinely broken release keeps taking traffic before the platform pulls it.

### Horizontal Auto-Scaling

Scale from 1 to N instances based on CPU and memory utilization thresholds. Load balancing across all instances is automatic.

Instances are added and removed on CPU and memory thresholds, and the count changes without telling you. That matters for anything holding state in process memory: an in-process cache, a rate-limiter counter, or a scheduler, each of which now exists once per replica rather than once in total.

### Framework Auto-Detection

Runsite detects your framework — Next.js, Django, Rails, FastAPI, and more — and configures the build pipeline automatically.

Detection covers the common build and start commands for Next.js, Nuxt, Django, Flask, Rails, Express, FastAPI and Spring Boot. When it guesses wrong, or the project is not one of those, a Dockerfile in the repository takes over and the platform builds exactly what you wrote.

Deploy Pipeline

## From git push to live

Every deploy follows the same reliable pipeline — from source code to running containers with zero downtime.

1

### Git Push Triggers Build

A webhook fires when you push to your configured branch. Runsite clones the repo, detects the framework (or reads your Dockerfile), and starts a containerized build with layer caching for faster rebuilds.

2

### Image Stored & Health Check Prepared

The built Docker image is stored in Runsite's internal registry. Environment variables, secrets, and health check configuration are attached to the deployment manifest.

3

### Rolling Deployment

New containers start alongside existing ones. Once health checks pass, traffic is gradually shifted to the new version. If anything fails, Runsite automatically rolls back to the previous healthy release.

4

### Live with SSL & Monitoring

Your app is live on its runsite.app subdomain (or custom domain) with automatic HTTPS. Real-time logs, CPU and memory metrics, and deploy history are available immediately in the dashboard.

Code

## Get Started in Minutes

Simple integration with your existing tools and workflows.

git-deploy.sh

```
# Push to deploy
git push origin main

# Runsite automatically:
# 1. Detects your framework
# 2. Builds your application
# 3. Deploys with zero downtime
```

Use Cases

## Built for Any Web Workload

Common scenarios where this service shines.

### Full-Stack Web Apps

Deploy Next.js, Nuxt, SvelteKit, or any full-stack framework with server-side rendering and API routes.

### REST & GraphQL APIs

Run Express, FastAPI, Django REST, or any API framework with automatic HTTPS and health monitoring.

### Mobile Backends

Build and deploy mobile app backends with WebSocket support, push notification services, and real-time features.

### Microservices

Deploy individual microservices with private networking, service discovery, and independent scaling.

EU-Sovereign

## EU Data Residency for Your Apps

Hosted in Germany — your data stays under EU jurisdiction.

Your application containers, environment variables, build artifacts, and logs run in Runsite's German region and never leave the EU. There is no replication to non-EU regions and no fallback to overseas infrastructure.

Every account is GDPR-compliant by default and includes a signed Data Processing Agreement at no extra cost and with no sales call. You can review how we handle data in our [Privacy Policy and DPA terms](https://runsite.app/privacy). That makes Runsite a fit when you handle EU customer data or fall under the GDPR or the EU Digital Services Act.

EU data residencyGDPR-readySigned DPA on every plan

Pricing

## Simple, transparent pricing

Pay only for the resources you actually use. Switch between monthly and hourly to see what you'll spend.

Runsite is in Public Beta

Every service listed here is live, and paid plans bill exactly as shown. Beta means the platform is young. Features still change, and parts of the roadmap are not built yet.

### Long-running web servers

·Apps · APIs · Backends

Push your repo, Runsite builds and serves. Auto-scale, zero-downtime rolling deploys, free SSL.

Web Services

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Disk

1 GB

Bandwidth

100 GB

[Choose Nano](https://dashboard.runsite.app/login)

Web Services

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

512 MB

Disk

5 GB

Bandwidth

1 TB

[Choose Starter](https://dashboard.runsite.app/login)

Web Services

### Standard

Popular

€12/mo

≈ €0.0164/hour, billed hourly

vCPU

1

RAM

1 GB

Disk

10 GB

Bandwidth

2 TB

[Choose Standard](https://dashboard.runsite.app/login)

Web Services

### Pro

€25/mo

≈ €0.0342/hour, billed hourly

vCPU

1

RAM

2 GB

Disk

10 GB

Bandwidth

5 TB

[Choose Pro](https://dashboard.runsite.app/login)

Web Services

### Pro Plus

€50/mo

≈ €0.0685/hour, billed hourly

vCPU

2

RAM

4 GB

Disk

20 GB

Bandwidth

10 TB

[Choose Pro Plus](https://dashboard.runsite.app/login)

Web Services

### Business

€95/mo

≈ €0.1301/hour, billed hourly

vCPU

4

RAM

8 GB

Disk

20 GB

Bandwidth

20 TB

[Choose Business](https://dashboard.runsite.app/login)

Web Services

### Business Plus

€180/mo

≈ €0.2466/hour, billed hourly

vCPU

8

RAM

16 GB

Disk

30 GB

Bandwidth

30 TB

[Choose Business Plus](https://dashboard.runsite.app/login)

Nano is the entry plan: the same 0.1 vCPU / 256 MB as before, now €3/mo and covered by your €5 starting credit. Nano services draw on a 100 GB monthly egress pool, ship a noindex header, and sleep after 14 days with no traffic. Starter and above are always-on, indexable and unmetered on egress.

### JAMstack & static frontends

·Landing pages · Docs · Portfolios

Deploy Next.js, Astro, Vite, Hugo and friends to a global CDN. Auto-deploy from Git, preview on every PR, free SSL.

Static Sites

### Free

Popular

Free

Never billed · never draws on your balance

CDN

Global

Bandwidth

100 GB

Builds

\~20s

Sites

Unlimited

[Deploy for free](https://dashboard.runsite.app/login)

Static sites are not billed. No plan, no hourly charge, and they never draw on your balance or credit. Pay-as-you-go bandwidth above the included quota will be introduced after launch. We'll notify you before any change.

### Production-ready databases

·Managed · Backups · EU-sovereign

One-click PostgreSQL with daily backups, connection pooling, encrypted at rest, never leaves the EU.

PostgreSQL

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Storage

1 GB

Connections

25

[Choose Nano](https://dashboard.runsite.app/login)

PostgreSQL

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

1 GB

Storage

5 GB

Connections

50

[Choose Starter](https://dashboard.runsite.app/login)

PostgreSQL

### Standard

Popular

€14/mo

≈ €0.0192/hour, billed hourly

vCPU

1

RAM

2 GB

Storage

10 GB

Connections

100

[Choose Standard](https://dashboard.runsite.app/login)

PostgreSQL

### Pro

€35/mo

≈ €0.0479/hour, billed hourly

vCPU

2

RAM

4 GB

Storage

20 GB

Connections

200

[Choose Pro](https://dashboard.runsite.app/login)

PostgreSQL

### Pro Plus

€70/mo

≈ €0.0959/hour, billed hourly

vCPU

4

RAM

8 GB

Storage

50 GB

Connections

400

[Choose Pro Plus](https://dashboard.runsite.app/login)

PostgreSQL

### Business

€130/mo

≈ €0.1781/hour, billed hourly

vCPU

8

RAM

16 GB

Storage

100 GB

Connections

500

[Choose Business](https://dashboard.runsite.app/login)

Storage beyond the included quota: +€0.25/GB/mo. Daily encrypted backups included. Nano is the entry plan at €3/mo. There is no longer a 30-day free database. If your credit and balance run out the database stops with its volume intact; press Start after topping up and it comes straight back. Storage is reclaimed 7 days later and the final backup is kept for 12 months.

### Sub-ms in-memory store

·Cache · Sessions · Queues

Drop-in Redis 7+ compatible store. Pub/Sub, automatic failover, TLS, RDB + AOF persistence.

Redis-compatible

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

Memory

256 MB

Connections

250

[Choose Starter](https://dashboard.runsite.app/login)

Redis-compatible

### Standard

Popular

€13/mo

≈ €0.0178/hour, billed hourly

Memory

1 GB

Connections

1,000

[Choose Standard](https://dashboard.runsite.app/login)

Redis-compatible

### Plus

€25/mo

≈ €0.0342/hour, billed hourly

Memory

2 GB

Connections

2,000

[Choose Plus](https://dashboard.runsite.app/login)

Redis-compatible

### Pro

€65/mo

≈ €0.0890/hour, billed hourly

Memory

5 GB

Connections

5,000

[Choose Pro](https://dashboard.runsite.app/login)

Redis-compatible

### Max

€125/mo

≈ €0.1712/hour, billed hourly

Memory

10 GB

Connections

10,000

[Choose Max](https://dashboard.runsite.app/login)

Redis starts at Starter. The 50 MB free instance is no longer offered. Every plan here includes RDB + AOF persistence, TLS and automatic failover, and is billed by the hour like everything else. A Starter instance runs for roughly a month on the €5 starting credit, if it is the only thing you have running.

Runsite hosts applications, not relays. VPN endpoints, proxies, VLESS / Xray / V2Ray and Shadowsocks nodes, and traffic tunnels of any kind are prohibited on every plan, paid ones included, and are blocked without notice or refund. See the Acceptable Use Policy in Section 4 of the Terms.

### Every plan, side by side

__Web Services plans, with monthly and hourly prices in euro__
| Plan          | vCPU | RAM    | Disk  | Bandwidth | Per month | Per hour |
| ------------- | ---- | ------ | ----- | --------- | --------- | -------- |
| Nano          | 0.1  | 256 MB | 1 GB  | 100 GB    | €3        | €0.0041  |
| Starter       | 0.5  | 512 MB | 5 GB  | 1 TB      | €5        | €0.0068  |
| Standard      | 1    | 1 GB   | 10 GB | 2 TB      | €12       | €0.0164  |
| Pro           | 1    | 2 GB   | 10 GB | 5 TB      | €25       | €0.0342  |
| Pro Plus      | 2    | 4 GB   | 20 GB | 10 TB     | €50       | €0.0685  |
| Business      | 4    | 8 GB   | 20 GB | 20 TB     | €95       | €0.1301  |
| Business Plus | 8    | 16 GB  | 30 GB | 30 TB     | €180      | €0.2466  |

[Compare all plans](https://runsite.app/pricing)[Try the calculator](https://runsite.app/pricing/calculator)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Can I run a VPN, proxy, or tunnel as a web service?

No. Proxies, VPN endpoints, VLESS, Xray, V2Ray and Shadowsocks nodes, and any service whose purpose is to forward third-party network traffic are prohibited on every plan, paid ones included. Services identified as running them are blocked without notice and without a refund. This is not a bandwidth cap we might raise later. Relay traffic saturates egress other customers paid for, and it lands our IP ranges on blocklists that take months to leave. An ordinary application that opens outbound connections — a scraper, a bot, a webhook relay, an API that calls other APIs — is fine. The line is whether the service exists to carry someone else's traffic. Section 4 of the Terms of Service sets this out in full.

### Which programming languages are supported?

Runsite supports any language that can run in a Docker container: Node.js, Python, Go, Ruby, Rust, Java, PHP, .NET, and more. For popular frameworks like Next.js, Django, Rails, and FastAPI, Runsite auto-detects the build configuration.

### How does auto-deploy work?

When you connect a Git repository, Runsite sets up a webhook. Every push to your configured branch triggers a new build and deploy automatically. The entire process — clone, build, deploy — typically completes in under 60 seconds.

### Can I use a custom Dockerfile?

Yes. If your repository contains a Dockerfile, Runsite uses it to build your application. You can specify a custom Dockerfile path and build context in the service settings.

### How does scaling work?

On Pro and Business plans, you can enable horizontal auto-scaling. Set minimum and maximum instance counts along with CPU/memory thresholds. Runsite automatically scales instances up during traffic spikes and down during quiet periods.

### What happens during a deploy?

Runsite performs a rolling deployment: new containers are started alongside old ones, health checks verify the new version is healthy, then traffic is shifted gradually. If health checks fail, Runsite automatically rolls back to the previous version.

Guides

## Learn More About Web Services

Hands-on articles from the Runsite blog.

[Deployment12 min readGit Push to Deploy: How Modern Web App Deployment Actually WorksWhat actually happens between git push and live traffic: build artifacts, health-gated releases, why a rollback isn't a rebuild, and the four things a deploy pipeline can't do for you.Read](https://runsite.app/blog/git-push-to-deploy)[Deployment15 min read8 European Heroku Alternatives in 2026 — and How to Check One Really Is EuropeanEight Heroku alternatives operated by European companies, checked on the column the lists leave out: which company you contract with, and where it is registered.Read](https://runsite.app/blog/european-heroku-alternative)[Deployment12 min readRender vs Railway: Pricing, Free Tiers, and the Question Neither AnswersEvery comparison argues per-second billing against fixed instances. That axis is real, and it is not the one that decides where your data lives.Read](https://runsite.app/blog/render-vs-railway)[Deployment7 min readWhat Are Cold Starts — and Why Your Free-Tier App Keeps Falling AsleepCold starts make the first request to a sleeping app painfully slow. Here's why free hosting tiers spin down, what it costs you, and how to keep an app warm without paying for idle capacity.Read](https://runsite.app/blog/cold-starts)[Compliance11 min readEU Hosting for Developers: Data Residency, GDPR, and DPAs in PracticeA customer asks where their data lives, or a deal stalls on a DPA request, and suddenly "we host in Europe" isn't a good enough answer. Here's what EU hosting actually means for developers: data residency, GDPR roles, DPAs, and how to build a stack that keeps data on EU soil.Read](https://runsite.app/blog/eu-hosting-for-developers)

Related

## You Might Also Need

Services that work great together.

[PostgreSQLManaged PostgreSQL. Daily backups, one-click restore. Just connect and start querying.Learn more](https://runsite.app/services/postgresql)[RedisIn-memory cache for sessions, queues, and real-time features. Toggle it on and go.Learn more](https://runsite.app/services/redis)[Email ServiceSend receipts, password resets, and notifications. 3,000 emails included every month.Learn more](https://runsite.app/services/email)

---

# Static Sites — Deploy JAMstack Apps in the EU

> Deploy static sites and JAMstack apps on EU infrastructure — Next.js, Astro, Hugo, Vite. Automatic builds, global CDN, preview URLs, free SSL.

1. [Home](https://runsite.app/)
2. [Services](https://runsite.app/services)
3. Static Sites

Compute EU · Germany

# Deploy Static Sites & JAMstack Apps, Hosted in the EU

Next.js, Astro, Hugo, Vite — any generator that writes static files. Automatic builds, a global CDN, free SSL, and your own domain.

[Start with €5 credit](https://dashboard.runsite.app/login)[Read the docs](https://docs.runsite.app/services/static-sites/)

Build Time

\~20 seconds

CDN Locations

10+ regions

Bandwidth

100 GB/mo

bash — build

 10 edges

 git push origin main

 astro build ................... 12.4s

 248 pages · 6.1 MB · fingerprinted

 purging cache, pushing to 10 regions

 deployed in 19s · TTFB 38ms

 https://docs-9b2.runsite.app 

Build output and origin files stored in Germany

Runtimes

Next.js · Gatsby · Hugo · Vite · Astro · Nuxt · SvelteKit · Remix · 11ty · Jekyll

Overview

Runsite Static Sites deploys pre-built static sites and JAMstack apps from Git. Push your code, Runsite runs your build, fingerprints the output for long-term caching, and serves it from a global CDN with automatic SSL on your own domain.

Builds and origin storage stay inside the European Union, so your content and build artifacts never leave EU borders. A signed GDPR Data Processing Agreement is part of every plan.

It works with Next.js static export, Astro, Hugo, Vite, SvelteKit, and any generator that writes static files to a publish directory. Every pull request gets its own preview URL to check before you merge. When a project needs server-side rendering or a backend process, deploy it as a [Web Service](https://runsite.app/services/web-services) instead, and keep user uploads or media in [S3-compatible object storage](https://runsite.app/services/s3-storage). Static sites are not billed at all: unlimited sites with 100 GB of bandwidth a month, no plan and no hourly charge.

Last reviewed: September 2026

At a glance

Build Time

\~20 seconds

CDN Locations

10+ regions

Bandwidth

100 GB/mo

SSL

Automatic

Deploys

Unlimited

Preview URLs

Per PR

Hosted in the EU · Germany

[Start with €5 credit](https://dashboard.runsite.app/login)

Features

## Everything to Deploy Static Sites

Built-in tools and capabilities to deploy and manage with confidence.

### Instant Builds

Optimized build pipeline with Docker layer caching reduces build times by up to 40%. Incremental builds for supported frameworks.

Docker layer caching means an unchanged dependency tree is not reinstalled, so most builds finish in about twenty seconds. Static sites are not billed at all, which means the build minutes do not draw on your balance or on the starting credit either.

### Global CDN

Assets served from edge locations worldwide for sub-100ms load times. Automatic cache invalidation on every deploy.

Files are served from edge locations rather than from Germany, so a visitor in Sydney gets the asset from nearby. The cache is invalidated on each deploy, which is why a stale page after a release is nearly always a browser or service-worker cache rather than the CDN.

### Preview Deploys

Every pull request gets a unique preview URL. Share with your team for visual review before merging to production.

Each pull request gets a stable URL that updates as you push to the branch, so the link you paste into a review thread stays correct. Previews are indexable only if you make them so; they carry no traffic and no production domain until you merge.

### Custom Domains & SSL

Map your own domain with automatic SSL certificate provisioning and renewal via Let's Encrypt. Supports apex and wildcard domains.

Certificates are issued and renewed through Let's Encrypt, which means a 90-day lifetime handled on a schedule rather than a setting you configure once. Apex and wildcard domains both work, and the renewal is the job that quietly breaks on servers you run yourself.

### Build Configuration

Specify publish directory, build commands, and environment variables. Framework auto-detection handles the defaults.

Publish directory, build command and environment variables are set per site, and detection fills in the defaults for the common generators. Variables are applied at build time for a static site, so changing one means a rebuild rather than a restart.

### Headers & Redirects

Configure custom response headers, URL redirects, and rewrite rules for SEO, security, and single-page app routing.

Response headers, redirects and rewrites are declared as rules rather than handled in application code, because there is no application process to handle them. Single-page apps need the rewrite to the index document here; without it, deep links return 404 on a hard refresh.

CDN Pipeline

## Build, cache, deliver globally

Static sites are built once and served from edge locations worldwide for blazing-fast load times.

1

### Build Triggered on Push

Runsite detects your framework, installs dependencies, and runs your build command. Docker layer caching reduces repeat build times by up to 40%.

2

### Assets Optimized & Hashed

Output files are fingerprinted with content hashes for infinite caching. HTML files get short cache TTLs so visitors always see fresh content.

3

### Distributed to Edge Locations

Built assets are pushed to 10+ edge locations worldwide. Every deploy triggers automatic cache invalidation across all nodes.

4

### Served with SSL & Custom Headers

Requests are routed to the nearest edge location. Custom response headers, redirects, and rewrite rules are applied at the edge layer.

Code

## Get Started in Minutes

Simple integration with your existing tools and workflows.

next.js-static-export.js

```
// next.config.js
module.exports = {
  output: 'export',
  images: { unoptimized: true },
}
```

Use Cases

## What Teams Build on Static Sites

Common scenarios where this service shines.

### Marketing Sites

Launch landing pages and marketing websites with blazing-fast load times and zero server management.

### Documentation

Deploy Docusaurus, VitePress, or GitBook documentation with auto-deploy on every content update.

### Portfolio & Blog

Deploy Gatsby, Astro, or Hugo blogs and portfolios with custom domains and global CDN distribution.

### E-commerce Storefronts

Build headless e-commerce frontends with frameworks like Next.js connected to any CMS or backend API.

EU-Sovereign

## Your Site Data, Stored in the EU

Hosted in Germany — your data stays under EU jurisdiction.

Your build pipeline runs and your origin files are stored in Runsite's German region. Public assets are cached at edge locations worldwide for speed, but the source of truth — your build output and configuration — stays inside the EU.

Every account is GDPR-compliant by default and includes a signed Data Processing Agreement at no extra cost and with no sales call. You can review how we handle data in our [Privacy Policy and DPA terms](https://runsite.app/privacy). That makes Runsite a fit when you handle EU customer data or fall under the GDPR or the EU Digital Services Act.

EU data residencyGDPR-readySigned DPA on every plan

Pricing

## Simple, transparent pricing

Pay only for the resources you actually use. Switch between monthly and hourly to see what you'll spend.

Runsite is in Public Beta

Every service listed here is live, and paid plans bill exactly as shown. Beta means the platform is young. Features still change, and parts of the roadmap are not built yet.

### Long-running web servers

·Apps · APIs · Backends

Push your repo, Runsite builds and serves. Auto-scale, zero-downtime rolling deploys, free SSL.

Web Services

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Disk

1 GB

Bandwidth

100 GB

[Choose Nano](https://dashboard.runsite.app/login)

Web Services

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

512 MB

Disk

5 GB

Bandwidth

1 TB

[Choose Starter](https://dashboard.runsite.app/login)

Web Services

### Standard

Popular

€12/mo

≈ €0.0164/hour, billed hourly

vCPU

1

RAM

1 GB

Disk

10 GB

Bandwidth

2 TB

[Choose Standard](https://dashboard.runsite.app/login)

Web Services

### Pro

€25/mo

≈ €0.0342/hour, billed hourly

vCPU

1

RAM

2 GB

Disk

10 GB

Bandwidth

5 TB

[Choose Pro](https://dashboard.runsite.app/login)

Web Services

### Pro Plus

€50/mo

≈ €0.0685/hour, billed hourly

vCPU

2

RAM

4 GB

Disk

20 GB

Bandwidth

10 TB

[Choose Pro Plus](https://dashboard.runsite.app/login)

Web Services

### Business

€95/mo

≈ €0.1301/hour, billed hourly

vCPU

4

RAM

8 GB

Disk

20 GB

Bandwidth

20 TB

[Choose Business](https://dashboard.runsite.app/login)

Web Services

### Business Plus

€180/mo

≈ €0.2466/hour, billed hourly

vCPU

8

RAM

16 GB

Disk

30 GB

Bandwidth

30 TB

[Choose Business Plus](https://dashboard.runsite.app/login)

Nano is the entry plan: the same 0.1 vCPU / 256 MB as before, now €3/mo and covered by your €5 starting credit. Nano services draw on a 100 GB monthly egress pool, ship a noindex header, and sleep after 14 days with no traffic. Starter and above are always-on, indexable and unmetered on egress.

### JAMstack & static frontends

·Landing pages · Docs · Portfolios

Deploy Next.js, Astro, Vite, Hugo and friends to a global CDN. Auto-deploy from Git, preview on every PR, free SSL.

Static Sites

### Free

Popular

Free

Never billed · never draws on your balance

CDN

Global

Bandwidth

100 GB

Builds

\~20s

Sites

Unlimited

[Deploy for free](https://dashboard.runsite.app/login)

Static sites are not billed. No plan, no hourly charge, and they never draw on your balance or credit. Pay-as-you-go bandwidth above the included quota will be introduced after launch. We'll notify you before any change.

### Production-ready databases

·Managed · Backups · EU-sovereign

One-click PostgreSQL with daily backups, connection pooling, encrypted at rest, never leaves the EU.

PostgreSQL

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Storage

1 GB

Connections

25

[Choose Nano](https://dashboard.runsite.app/login)

PostgreSQL

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

1 GB

Storage

5 GB

Connections

50

[Choose Starter](https://dashboard.runsite.app/login)

PostgreSQL

### Standard

Popular

€14/mo

≈ €0.0192/hour, billed hourly

vCPU

1

RAM

2 GB

Storage

10 GB

Connections

100

[Choose Standard](https://dashboard.runsite.app/login)

PostgreSQL

### Pro

€35/mo

≈ €0.0479/hour, billed hourly

vCPU

2

RAM

4 GB

Storage

20 GB

Connections

200

[Choose Pro](https://dashboard.runsite.app/login)

PostgreSQL

### Pro Plus

€70/mo

≈ €0.0959/hour, billed hourly

vCPU

4

RAM

8 GB

Storage

50 GB

Connections

400

[Choose Pro Plus](https://dashboard.runsite.app/login)

PostgreSQL

### Business

€130/mo

≈ €0.1781/hour, billed hourly

vCPU

8

RAM

16 GB

Storage

100 GB

Connections

500

[Choose Business](https://dashboard.runsite.app/login)

Storage beyond the included quota: +€0.25/GB/mo. Daily encrypted backups included. Nano is the entry plan at €3/mo. There is no longer a 30-day free database. If your credit and balance run out the database stops with its volume intact; press Start after topping up and it comes straight back. Storage is reclaimed 7 days later and the final backup is kept for 12 months.

### Sub-ms in-memory store

·Cache · Sessions · Queues

Drop-in Redis 7+ compatible store. Pub/Sub, automatic failover, TLS, RDB + AOF persistence.

Redis-compatible

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

Memory

256 MB

Connections

250

[Choose Starter](https://dashboard.runsite.app/login)

Redis-compatible

### Standard

Popular

€13/mo

≈ €0.0178/hour, billed hourly

Memory

1 GB

Connections

1,000

[Choose Standard](https://dashboard.runsite.app/login)

Redis-compatible

### Plus

€25/mo

≈ €0.0342/hour, billed hourly

Memory

2 GB

Connections

2,000

[Choose Plus](https://dashboard.runsite.app/login)

Redis-compatible

### Pro

€65/mo

≈ €0.0890/hour, billed hourly

Memory

5 GB

Connections

5,000

[Choose Pro](https://dashboard.runsite.app/login)

Redis-compatible

### Max

€125/mo

≈ €0.1712/hour, billed hourly

Memory

10 GB

Connections

10,000

[Choose Max](https://dashboard.runsite.app/login)

Redis starts at Starter. The 50 MB free instance is no longer offered. Every plan here includes RDB + AOF persistence, TLS and automatic failover, and is billed by the hour like everything else. A Starter instance runs for roughly a month on the €5 starting credit, if it is the only thing you have running.

Runsite hosts applications, not relays. VPN endpoints, proxies, VLESS / Xray / V2Ray and Shadowsocks nodes, and traffic tunnels of any kind are prohibited on every plan, paid ones included, and are blocked without notice or refund. See the Acceptable Use Policy in Section 4 of the Terms.

### Every plan, side by side

__Static Sites plans, with monthly and hourly prices in euro__
| Plan | CDN    | Bandwidth | Builds | Sites     | Per month | Per hour |
| ---- | ------ | --------- | ------ | --------- | --------- | -------- |
| Free | Global | 100 GB    | \~20s  | Unlimited | Free      | Free     |

[Compare all plans](https://runsite.app/pricing)[Try the calculator](https://runsite.app/pricing/calculator)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What static site generators are supported?

Runsite supports all major static site generators and frameworks: Next.js (static export), Gatsby, Hugo, Vite, Astro, Nuxt, SvelteKit, Remix, 11ty, Jekyll, and any tool that outputs static HTML/CSS/JS to a publish directory.

### How is a static site different from a web service?

Static sites are pre-built at deploy time and served as static files via CDN, resulting in faster load times and lower cost. Web services run a server process (Node.js, Python, etc.) and handle dynamic requests. Choose static for content-focused sites and web services for dynamic applications.

### Can I use server-side rendering with static sites?

Static sites on Runsite serve pre-built files only. If you need server-side rendering (SSR), deploy as a Web Service instead. Frameworks like Next.js support both — use static export for static, or deploy as a web service for SSR.

### How does CDN caching work?

When you deploy, Runsite distributes your built assets to edge locations worldwide. Cache is automatically invalidated on every new deploy, so visitors always see the latest version. Assets with content hashes are cached indefinitely for optimal performance.

Guides

## Learn More About Static Sites

Hands-on articles from the Runsite blog.

[Deployment12 min readGit Push to Deploy: How Modern Web App Deployment Actually WorksWhat actually happens between git push and live traffic: build artifacts, health-gated releases, why a rollback isn't a rebuild, and the four things a deploy pipeline can't do for you.Read](https://runsite.app/blog/git-push-to-deploy)[Deployment7 min readWhat Are Cold Starts — and Why Your Free-Tier App Keeps Falling AsleepCold starts make the first request to a sleeping app painfully slow. Here's why free hosting tiers spin down, what it costs you, and how to keep an app warm without paying for idle capacity.Read](https://runsite.app/blog/cold-starts)[Compliance11 min readEU Hosting for Developers: Data Residency, GDPR, and DPAs in PracticeA customer asks where their data lives, or a deal stalls on a DPA request, and suddenly "we host in Europe" isn't a good enough answer. Here's what EU hosting actually means for developers: data residency, GDPR roles, DPAs, and how to build a stack that keeps data on EU soil.Read](https://runsite.app/blog/eu-hosting-for-developers)

Related

## You Might Also Need

Services that work great together.

[Web ServicesNode.js, Python, Go, Ruby, Rust, or your own Dockerfile — Runsite builds it, runs it, and keeps it online on European infrastructure.Learn more](https://runsite.app/services/web-services)[S3 Object StorageStore files, images, and backups. S3-compatible — use the tools you already know.Learn more](https://runsite.app/services/s3-storage)[PostgreSQLManaged PostgreSQL. Daily backups, one-click restore. Just connect and start querying.Learn more](https://runsite.app/services/postgresql)

---

# Cron Jobs — Managed Scheduled Tasks on Runsite

> Schedule background jobs on Runsite: Docker containers triggered by cron expressions, timezone support, concurrency policies, execution history and failure alerts.

1. [Home](https://runsite.app/)
2. [Services](https://runsite.app/services)
3. Cron Jobs

Coming soon

Cron Jobs aren't live yet — we're finishing the runtime and the dashboard. Sign up and we'll ping you the moment it ships.

Compute EU · Germany

# Managed Cron Jobs & Scheduled Tasks

Run scheduled tasks automatically. Set a schedule, we run your code — with logs and retries built in.

[Start with €5 credit](https://dashboard.runsite.app/login)[Read the docs](https://docs.runsite.app)

CPU

Up to 2 vCPU

Memory

Up to 512 MB

Timeout

Up to 1 hour

runsite — schedules

 3 jobs

 runsite cron ls

 0 3 * * *     pg-backup       ok · 41s

 */15 * * * *  sync-inventory  ok · 2.4s

 0 9 * * 1     weekly-digest   ok · 12s

 next run 03:00 CET

 retries 3 · overlap skip · history 30d 

Jobs are provisioned and executed in Germany

Overview

Runsite Cron Jobs runs scheduled tasks as containers, so backups, report generation, data syncs, and cleanup jobs happen without a server to babysit. Write a standard Unix cron expression, point it at a Docker image, and Runsite takes care of provisioning, timeouts, retries, and logging.

Jobs execute on infrastructure inside the European Union, so whatever data they read or write stays within EU borders. A signed GDPR Data Processing Agreement is included on every plan.

Every run is recorded with its exit code, output, and duration, and a failed run can alert you by email or webhook. Concurrency policies decide what happens when a run overlaps the previous one, so long jobs never pile up. Most jobs sit alongside the rest of your stack: dump a [PostgreSQL](https://runsite.app/services/postgresql) backup to [object storage](https://runsite.app/services/s3-storage), or refresh caches and data for your [web services](https://runsite.app/services/web-services).

Last reviewed: September 2026

At a glance

CPU

Up to 2 vCPU

Memory

Up to 512 MB

Timeout

Up to 1 hour

Min Interval

Every minute

History

30 days

Concurrency

Configurable

Hosted in the EU · Germany

[Start with €5 credit](https://dashboard.runsite.app/login)

Features

## Everything to Run Scheduled Jobs

Built-in tools and capabilities to deploy and manage with confidence.

### Cron Schedule Syntax

Use standard Unix cron expressions to define when your jobs run — every minute, hourly, daily, weekly, or any custom interval.

Standard five-field Unix expressions, so the schedules you already have transfer unchanged. The scheduler lives outside your application, which is the difference that matters: an in-process timer only fires while that process is running, and a container that has been scaled to zero fires nothing.

### Timezone Support

Schedule jobs in any timezone. Run reports at 9 AM local time or batch processes at midnight in your users' timezone.

Schedules are evaluated in the timezone you pick rather than in UTC, which matters for anything tied to a working day. Note that daylight saving makes one local hour happen twice a year and one not at all, so a job at 02:30 needs an idempotent body.

### Concurrency Policies

Choose Allow, Forbid, or Replace policies to control what happens when a new run is triggered while a previous one is still executing.

Allow starts the new run regardless, Forbid skips it while the previous one is still going, and Replace stops the old one first. The default worth thinking about is Forbid: it is what keeps a job that has begun to run long from stacking copies of itself until memory runs out.

### Custom Docker Images

Run any script or binary in a Docker container. Bring your own image or use Runsite's built-in runtime images for popular languages.

Any image runs, so a job can be a shell script, a management command, or a compiled binary with no runtime around it. Using the same image as your web service means the job sees the same dependencies and the same code, which removes a whole class of drift.

### Execution History

Track every execution with status, duration, and logs. Set retention limits for successful and failed job history.

Every run records its exit code, its output and its duration, so a job that has been failing silently for a week is visible rather than inferred. Retention is configurable separately for successful and failed runs, since the failures are the ones worth keeping.

### Failure Alerts

Get notified when a job fails or exceeds its timeout. Configure retry behavior and alert channels for critical tasks.

A non-zero exit or a timeout raises an alert by email or webhook. This is the part a crontab on your own server does not do: there, a failed run writes to a log nobody reads, and the first sign of trouble is the report that never arrived.

Execution Lifecycle

## Schedule, run, monitor

Every scheduled job follows a predictable lifecycle with built-in safety nets for failures.

1

### Schedule Defined

Set a cron expression (e.g. 0 2 \* \* \* for daily at 2 AM) with a timezone. Choose a concurrency policy: Allow, Forbid, or Replace.

2

### Container Provisioned

At the scheduled time, Runsite spins up a fresh container from your Docker image with your environment variables and secrets injected.

3

### Execution & Timeout Guard

Your script runs inside the container. If it exceeds the timeout limit, the container is terminated and the run is marked as failed with an alert sent.

4

### Logs & History Recorded

Exit code, stdout/stderr output, duration, and resource usage are recorded. Failed runs trigger configurable alerts via email or webhook.

Code

## Get Started in Minutes

Simple integration with your existing tools and workflows.

daily-backup.sh

```
# Schedule: 0 2 * * * (daily at 2 AM)
# Timeout: 3600s

#!/bin/bash
pg_dump $DATABASE_URL | gzip > backup.sql.gz
aws s3 cp backup.sql.gz s3://backups/$(date +%F).sql.gz
```

Use Cases

## What You Can Automate

Common scenarios where this service shines.

### Database Maintenance

Schedule backups, cleanup old records, run data migrations, and generate reports on a recurring basis.

### Email Digests

Send daily or weekly digest emails, notification summaries, and scheduled marketing campaigns.

### Data Sync

Synchronize data between services, pull from external APIs, update caches, and refresh materialized views.

### Analytics & Reports

Generate daily analytics snapshots, compute aggregated metrics, and export reports to external systems.

EU-Sovereign

## Your Jobs Run Inside the EU

Hosted in Germany — your data stays under EU jurisdiction.

Scheduled containers are provisioned and executed in Runsite's German region. Any database rows, files, or API responses your job touches are processed inside the EU, and execution logs are stored there too.

Every account is GDPR-compliant by default and includes a signed Data Processing Agreement at no extra cost and with no sales call. You can review how we handle data in our [Privacy Policy and DPA terms](https://runsite.app/privacy). That makes Runsite a fit when you handle EU customer data or fall under the GDPR or the EU Digital Services Act.

EU data residencyGDPR-readySigned DPA on every plan

Planned pricing

## Simple, transparent pricing

This service is still in development. The plans below are a preview — final pricing will be confirmed at launch.

Runsite is in Public Beta

Every service listed here is live, and paid plans bill exactly as shown. Beta means the platform is young. Features still change, and parts of the roadmap are not built yet.

### Starter

€3/mo

* 5 cron jobs
* Per-minute scheduling
* Failure alerts
* 14-day history
[Join the waitlist](https://dashboard.runsite.app/login)

### Pro

€12/mo

* Unlimited cron jobs
* Custom Docker images
* Extended timeout (1 hour)
* 30-day history
[Join the waitlist](https://dashboard.runsite.app/login)

### Every plan, side by side

__Cron Jobs plans and what each includes__
| Plan    | Price  | What it includes                                                                     |
| ------- | ------ | ------------------------------------------------------------------------------------ |
| Starter | €3/mo  | 5 cron jobs. Per-minute scheduling. Failure alerts. 14-day history                   |
| Pro     | €12/mo | Unlimited cron jobs. Custom Docker images. Extended timeout (1 hour). 30-day history |

[Compare all plans](https://runsite.app/pricing)[Try the calculator](https://runsite.app/pricing/calculator)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What is the maximum execution time for a cron job?

The default timeout is 3600 seconds (1 hour). On the Business plan, you can request extended timeouts for long-running batch processes. If a job exceeds its timeout, it is terminated and marked as failed.

### What happens if a job is still running when the next execution is scheduled?

You can configure a concurrency policy: Allow (run in parallel), Forbid (skip the new run), or Replace (cancel the running job and start a new one). The default policy is Forbid.

### Can I run a cron job manually?

Yes. You can trigger any cron job manually from the dashboard at any time, regardless of its schedule. This is useful for testing or running one-off executions.

### How are cron job failures handled?

Failed jobs are logged with their exit code and output. You can configure alert notifications via email or webhook. The execution history shows all past runs with their status, duration, and logs for debugging.

Guides

## Learn More About Cron Jobs

Hands-on articles from the Runsite blog.

[Databases7 min readHow to Automate PostgreSQL Backups to S3 on a ScheduleA managed database backs itself up daily — but a second, portable copy in your own S3 bucket is yours to keep. Here's how to schedule pg\_dump to object storage with a cron job, and when it's worth doing.Read](https://runsite.app/blog/postgresql-backup-to-s3)[Databases10 min readPoint-in-Time Recovery: How WAL Archiving Saves You After a DROP TABLEYour nightly backup answers "do we have a copy?" Point-in-time recovery answers "can we go back to 3:06pm?" Here's what the write-ahead log stores, how PITR replays it, and what it still can't undo.Read](https://runsite.app/blog/postgres-point-in-time-recovery)[Deployment7 min readWhat Are Cold Starts — and Why Your Free-Tier App Keeps Falling AsleepCold starts make the first request to a sleeping app painfully slow. Here's why free hosting tiers spin down, what it costs you, and how to keep an app warm without paying for idle capacity.Read](https://runsite.app/blog/cold-starts)

Related

## You Might Also Need

Services that work great together.

[Web ServicesNode.js, Python, Go, Ruby, Rust, or your own Dockerfile — Runsite builds it, runs it, and keeps it online on European infrastructure.Learn more](https://runsite.app/services/web-services)[PostgreSQLManaged PostgreSQL. Daily backups, one-click restore. Just connect and start querying.Learn more](https://runsite.app/services/postgresql)[Email ServiceSend receipts, password resets, and notifications. 3,000 emails included every month.Learn more](https://runsite.app/services/email)

---

# Managed PostgreSQL Service — EU Database Hosting

> Managed PostgreSQL on EU infrastructure: daily encrypted backups, one-click restore, built-in PgBouncer pooling and vertical scaling. Data never leaves Germany.

1. [Home](https://runsite.app/)
2. [Services](https://runsite.app/services)
3. PostgreSQL

Data EU · Germany

# Managed PostgreSQL Hosting in the EU

Managed PostgreSQL. Daily backups, one-click restore. Just connect and start querying.

[Start with €5 credit](https://dashboard.runsite.app/login)[Read the docs](https://docs.runsite.app/services/postgresql/)

CPU

Up to 8 vCPU

Storage

Up to 100 GB

Version

PostgreSQL 16

psql — runsite

 pg 16.4

 psql $DATABASE_URL

 psql (16.4) · TLS · PgBouncer pooling

 SELECT extname FROM pg_extension;

 pgvector · postgis · pg_trgm

 \! runsite db backups

 daily + PITR · 30-day window 

Instance, WAL archive and backups never leave the EU

Overview

Runsite Managed PostgreSQL is PostgreSQL as a service: a PostgreSQL 16 database you can spin up in seconds and connect to from any service. Provisioning, daily backups, point-in-time recovery, connection pooling, and version upgrades are handled for you, so you spend your time writing queries instead of maintaining a server.

Every database lives entirely within the European Union, and so do its backups. Each account is GDPR-compliant by default with a signed Data Processing Agreement, no upcharge and no sales call.

It supports the extensions people actually reach for: PostGIS for geospatial data, pgvector for AI embeddings, and pg\_trgm for fuzzy search, with built-in PgBouncer pooling and one-click vertical scaling. Put [Redis](https://runsite.app/services/redis) in front of it to cache hot queries, connect it to your [web services](https://runsite.app/services/web-services), or stream nightly backups to [object storage](https://runsite.app/services/s3-storage). The entry Nano plan is €3/mo for a 1 GB database with daily backups, covered by the €5 credit every new account starts with, so you can begin without a credit card and scale up when the project is real.

Last reviewed: September 2026

At a glance

CPU

Up to 8 vCPU

Storage

Up to 100 GB

Version

PostgreSQL 16

Backups

Daily + PITR

Networking

Private + Public

Extensions

PostGIS, pgvector, +

Hosted in the EU · Germany

[Start with €5 credit](https://dashboard.runsite.app/login)

Features

## Everything a Production Database Needs

Built-in tools and capabilities to deploy and manage with confidence.

### Automated Backups

Daily automated backups retained for up to 30 days. Restore to any point in time within your retention window with a single click.

A daily snapshot answers whether a copy exists. Continuous write-ahead log archiving answers a different question, which is whether you can return to the moment before a bad migration ran. Both are kept inside the retention window, and both live in the provider's account rather than yours.

### Read Replicas

Add read replicas on Business plans to distribute read traffic and improve query performance for read-heavy workloads.

A replica takes read traffic off the primary, which helps a read-heavy workload and does nothing for a write-heavy one. Replication is asynchronous, so a read issued immediately after a write can still see the old row; route anything that must be read-your-writes to the primary.

### Connection Pooling

Built-in PgBouncer connection pooling optimizes resource usage under high concurrency. No external pooler needed.

PgBouncer runs in front of the database on port 6432 in transaction mode, so thousands of clients share a handful of real backends. Port 5432 stays available for the work that needs session state — migrations, LISTEN/NOTIFY, advisory locks and prepared statements you cannot disable.

### Monitoring Dashboard

Real-time metrics for connections, queries per second, memory usage, storage utilization, and replication lag.

Connections, queries per second, memory, storage and replication lag, read live. The connection count is the one worth an alert: it is a hard ceiling rather than a soft one, and hitting it produces a refusal to connect rather than a slowdown you can watch coming.

### One-Click Scaling

Scale CPU, memory, and storage vertically with a single click. Storage autoscaling prevents disk-full emergencies.

Vertical scaling raises CPU, memory and storage together, which matters because a higher connection limit without more memory buys instability rather than headroom. Storage grows automatically ahead of a disk-full event, which is the failure that takes a database down rather than slowing it.

### Private Networking

Connect over private internal networking for lower latency and enhanced security. TLS encryption enabled by default.

Services inside Runsite reach the database over internal networking rather than the public internet, which removes a hop and a class of exposure. TLS is on either way, so an external client — a migration from your laptop, a BI tool — connects safely without a tunnel.

Backup & Recovery

## Your data is always safe

Automated backups and point-in-time recovery ensure you never lose data, even in the worst-case scenario.

1

### Continuous WAL Archiving

Write-Ahead Log segments are continuously streamed to durable storage. This enables point-in-time recovery (PITR) to any second within your retention window.

2

### Daily Automated Snapshots

A full database snapshot is taken daily during low-traffic hours. Snapshots are stored redundantly and retained for 7 to 30 days depending on your plan.

3

### One-Click Restore

Restore to any point in time from the dashboard — pick a timestamp and Runsite provisions a new database with your data as it was at that exact moment.

4

### Scaling Without Downtime

Vertical scaling (CPU, RAM, storage) is performed with minimal downtime via a controlled failover. Storage autoscaling prevents disk-full emergencies before they happen.

Code

## Get Started in Minutes

Simple integration with your existing tools and workflows.

connection-string.sh

```
# Add to your environment variables
DATABASE_URL=postgresql://user:password@db.runsite.app:5432/mydb

# With connection pooling
DATABASE_URL=postgresql://user:password@db.runsite.app:6432/mydb?pgbouncer=true
```

Use Cases

## What You Can Build on PostgreSQL

Common scenarios where this service shines.

### Web Application Data

Store user accounts, content, transactions, and application state for your web services with ACID guarantees.

### Analytics & Reporting

Run complex analytical queries with PostgreSQL's powerful window functions, CTEs, and JSON operators.

### Geospatial Applications

Use PostGIS extension for location-based queries, mapping applications, and geospatial analytics.

### Full-Text Search

Leverage PostgreSQL's built-in full-text search with tsvector and tsquery for fast, relevance-ranked results.

EU-Sovereign

## Your Database Lives in the EU

Hosted in Germany — your data stays under EU jurisdiction.

Your PostgreSQL instance, its WAL archive, and every automated backup are stored in Runsite's German region and never leave EU borders. Point-in-time recovery snapshots stay in the same region, so a restore never pulls data across a non-EU boundary.

Every account is GDPR-compliant by default and includes a signed Data Processing Agreement at no extra cost and with no sales call. You can review how we handle data in our [Privacy Policy and DPA terms](https://runsite.app/privacy). That makes Runsite a fit when you handle EU customer data or fall under the GDPR or the EU Digital Services Act.

EU data residencyGDPR-readySigned DPA on every plan

Pricing

## Simple, transparent pricing

Pay only for the resources you actually use. Switch between monthly and hourly to see what you'll spend.

Runsite is in Public Beta

Every service listed here is live, and paid plans bill exactly as shown. Beta means the platform is young. Features still change, and parts of the roadmap are not built yet.

### Long-running web servers

·Apps · APIs · Backends

Push your repo, Runsite builds and serves. Auto-scale, zero-downtime rolling deploys, free SSL.

Web Services

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Disk

1 GB

Bandwidth

100 GB

[Choose Nano](https://dashboard.runsite.app/login)

Web Services

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

512 MB

Disk

5 GB

Bandwidth

1 TB

[Choose Starter](https://dashboard.runsite.app/login)

Web Services

### Standard

Popular

€12/mo

≈ €0.0164/hour, billed hourly

vCPU

1

RAM

1 GB

Disk

10 GB

Bandwidth

2 TB

[Choose Standard](https://dashboard.runsite.app/login)

Web Services

### Pro

€25/mo

≈ €0.0342/hour, billed hourly

vCPU

1

RAM

2 GB

Disk

10 GB

Bandwidth

5 TB

[Choose Pro](https://dashboard.runsite.app/login)

Web Services

### Pro Plus

€50/mo

≈ €0.0685/hour, billed hourly

vCPU

2

RAM

4 GB

Disk

20 GB

Bandwidth

10 TB

[Choose Pro Plus](https://dashboard.runsite.app/login)

Web Services

### Business

€95/mo

≈ €0.1301/hour, billed hourly

vCPU

4

RAM

8 GB

Disk

20 GB

Bandwidth

20 TB

[Choose Business](https://dashboard.runsite.app/login)

Web Services

### Business Plus

€180/mo

≈ €0.2466/hour, billed hourly

vCPU

8

RAM

16 GB

Disk

30 GB

Bandwidth

30 TB

[Choose Business Plus](https://dashboard.runsite.app/login)

Nano is the entry plan: the same 0.1 vCPU / 256 MB as before, now €3/mo and covered by your €5 starting credit. Nano services draw on a 100 GB monthly egress pool, ship a noindex header, and sleep after 14 days with no traffic. Starter and above are always-on, indexable and unmetered on egress.

### JAMstack & static frontends

·Landing pages · Docs · Portfolios

Deploy Next.js, Astro, Vite, Hugo and friends to a global CDN. Auto-deploy from Git, preview on every PR, free SSL.

Static Sites

### Free

Popular

Free

Never billed · never draws on your balance

CDN

Global

Bandwidth

100 GB

Builds

\~20s

Sites

Unlimited

[Deploy for free](https://dashboard.runsite.app/login)

Static sites are not billed. No plan, no hourly charge, and they never draw on your balance or credit. Pay-as-you-go bandwidth above the included quota will be introduced after launch. We'll notify you before any change.

### Production-ready databases

·Managed · Backups · EU-sovereign

One-click PostgreSQL with daily backups, connection pooling, encrypted at rest, never leaves the EU.

PostgreSQL

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Storage

1 GB

Connections

25

[Choose Nano](https://dashboard.runsite.app/login)

PostgreSQL

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

1 GB

Storage

5 GB

Connections

50

[Choose Starter](https://dashboard.runsite.app/login)

PostgreSQL

### Standard

Popular

€14/mo

≈ €0.0192/hour, billed hourly

vCPU

1

RAM

2 GB

Storage

10 GB

Connections

100

[Choose Standard](https://dashboard.runsite.app/login)

PostgreSQL

### Pro

€35/mo

≈ €0.0479/hour, billed hourly

vCPU

2

RAM

4 GB

Storage

20 GB

Connections

200

[Choose Pro](https://dashboard.runsite.app/login)

PostgreSQL

### Pro Plus

€70/mo

≈ €0.0959/hour, billed hourly

vCPU

4

RAM

8 GB

Storage

50 GB

Connections

400

[Choose Pro Plus](https://dashboard.runsite.app/login)

PostgreSQL

### Business

€130/mo

≈ €0.1781/hour, billed hourly

vCPU

8

RAM

16 GB

Storage

100 GB

Connections

500

[Choose Business](https://dashboard.runsite.app/login)

Storage beyond the included quota: +€0.25/GB/mo. Daily encrypted backups included. Nano is the entry plan at €3/mo. There is no longer a 30-day free database. If your credit and balance run out the database stops with its volume intact; press Start after topping up and it comes straight back. Storage is reclaimed 7 days later and the final backup is kept for 12 months.

### Sub-ms in-memory store

·Cache · Sessions · Queues

Drop-in Redis 7+ compatible store. Pub/Sub, automatic failover, TLS, RDB + AOF persistence.

Redis-compatible

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

Memory

256 MB

Connections

250

[Choose Starter](https://dashboard.runsite.app/login)

Redis-compatible

### Standard

Popular

€13/mo

≈ €0.0178/hour, billed hourly

Memory

1 GB

Connections

1,000

[Choose Standard](https://dashboard.runsite.app/login)

Redis-compatible

### Plus

€25/mo

≈ €0.0342/hour, billed hourly

Memory

2 GB

Connections

2,000

[Choose Plus](https://dashboard.runsite.app/login)

Redis-compatible

### Pro

€65/mo

≈ €0.0890/hour, billed hourly

Memory

5 GB

Connections

5,000

[Choose Pro](https://dashboard.runsite.app/login)

Redis-compatible

### Max

€125/mo

≈ €0.1712/hour, billed hourly

Memory

10 GB

Connections

10,000

[Choose Max](https://dashboard.runsite.app/login)

Redis starts at Starter. The 50 MB free instance is no longer offered. Every plan here includes RDB + AOF persistence, TLS and automatic failover, and is billed by the hour like everything else. A Starter instance runs for roughly a month on the €5 starting credit, if it is the only thing you have running.

Runsite hosts applications, not relays. VPN endpoints, proxies, VLESS / Xray / V2Ray and Shadowsocks nodes, and traffic tunnels of any kind are prohibited on every plan, paid ones included, and are blocked without notice or refund. See the Acceptable Use Policy in Section 4 of the Terms.

### Every plan, side by side

__PostgreSQL plans, with monthly and hourly prices in euro__
| Plan     | vCPU | RAM    | Storage | Connections | Per month | Per hour |
| -------- | ---- | ------ | ------- | ----------- | --------- | -------- |
| Nano     | 0.1  | 256 MB | 1 GB    | 25          | €3        | €0.0041  |
| Starter  | 0.5  | 1 GB   | 5 GB    | 50          | €5        | €0.0068  |
| Standard | 1    | 2 GB   | 10 GB   | 100         | €14       | €0.0192  |
| Pro      | 2    | 4 GB   | 20 GB   | 200         | €35       | €0.0479  |
| Pro Plus | 4    | 8 GB   | 50 GB   | 400         | €70       | €0.0959  |
| Business | 8    | 16 GB  | 100 GB  | 500         | €130      | €0.1781  |

[Compare all plans](https://runsite.app/pricing)[Try the calculator](https://runsite.app/pricing/calculator)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Which PostgreSQL version is supported?

Runsite runs PostgreSQL 16 by default, the latest stable release. We support versions 14, 15, and 16\. Major version upgrades can be performed with minimal downtime via the dashboard.

### What happens if my balance runs out?

The database stops, with its volume intact. There is no longer a 30-day free database: the entry plan is Nano at €3/mo, and every new account starts with €5 of credit valid for 30 days. We email you on day 23 and day 29 of the credit, and again when the database actually stops. Top up any amount from €5 and press Start — it comes back exactly where it left off, same data, same connection string. Storage is held for 7 days after the stop. After that the volume is reclaimed and the final backup set (the last 7 daily backups) is kept for 12 months, so a project you return to months later can still be restored by topping up.

### How do automated backups work?

Runsite takes automated daily snapshots of your database and continuously archives WAL (Write-Ahead Log) segments. This enables point-in-time recovery (PITR) — you can restore your database to any second within the backup retention window (7 days on Pro, 30 days on Business).

### Can I connect from outside Runsite?

Yes. Each database has both an internal hostname (for services within Runsite) and an external hostname (for connections from your local machine or external services). External connections use TLS encryption by default.

### Are PostgreSQL extensions supported?

Yes. Runsite supports popular extensions including PostGIS (geospatial), pgvector (AI embeddings), pg\_trgm (fuzzy search), uuid-ossp, hstore, and more. Contact support if you need an extension that is not pre-installed.

### How does connection pooling work?

Runsite includes built-in PgBouncer connection pooling. Use the pooler connection string (port 6432) to multiplex many application connections over a smaller number of database connections, reducing memory usage and improving performance under high concurrency.

Guides

## Learn More About PostgreSQL

Hands-on articles from the Runsite blog.

[Databases11 min readManaged PostgreSQL for Developers: What the Host Actually Takes Off Your PlateInstalling Postgres takes a minute. Running it in production for three years is the real job. Here's the specific list of operational chores that move to the host when a database is "managed" — and the ones that stay yours no matter what.Read](https://runsite.app/blog/managed-postgresql-for-developers)[Databases11 min readSelf-Hosted vs Managed PostgreSQL: The Real Cost of OwnershipThe €5 VPS looks cheaper than the €14 managed plan — until you price the hours. A line-by-line cost of ownership for self-hosting Postgres versus managed, the risk nobody puts on the invoice, and when self-hosting actually wins.Read](https://runsite.app/blog/self-hosted-vs-managed-postgresql)[Databases10 min readPostgreSQL "FATAL: sorry, too many clients already" — What It Means and How to Fix ItYour app is suddenly throwing 500s and the database logs are full of "too many clients already." Here's what the error really means, how to see what's hogging your connections, and the fix that actually holds: pooling, not a bigger number.Read](https://runsite.app/blog/postgres-too-many-clients-already)[Databases10 min readConnection Pooling with PgBouncer: What Ports 5432 and 6432 Really DoYou switched to port 6432 to stop the connection errors, and now prepared statements throw and SET doesn't stick. That's pooling working as designed. Here's what a pooler actually does, why 5432 and 6432 are different contracts, and what breaks in transaction mode.Read](https://runsite.app/blog/pgbouncer-connection-pooling)[Databases10 min readPoint-in-Time Recovery: How WAL Archiving Saves You After a DROP TABLEYour nightly backup answers "do we have a copy?" Point-in-time recovery answers "can we go back to 3:06pm?" Here's what the write-ahead log stores, how PITR replays it, and what it still can't undo.Read](https://runsite.app/blog/postgres-point-in-time-recovery)[Databases11 min readpgvector: Storing and Searching AI Embeddings in PostgreSQLYour app called an embeddings API and got back 1,536 numbers per document. Before you add a vector database to the stack: what pgvector stores, how its two indexes trade recall for speed, and when Postgres alone carries a semantic search feature.Read](https://runsite.app/blog/pgvector-embeddings-postgresql)

Related

## You Might Also Need

Services that work great together.

[Web ServicesNode.js, Python, Go, Ruby, Rust, or your own Dockerfile — Runsite builds it, runs it, and keeps it online on European infrastructure.Learn more](https://runsite.app/services/web-services)[RedisIn-memory cache for sessions, queues, and real-time features. Toggle it on and go.Learn more](https://runsite.app/services/redis)[S3 Object StorageStore files, images, and backups. S3-compatible — use the tools you already know.Learn more](https://runsite.app/services/s3-storage)

---

# Managed Redis — One-Click Cache & Data Store on Runsite

> Managed Redis-compatible caching on EU infrastructure: sessions, queues and Pub/Sub with RDB and AOF persistence, TLS and automatic failover. From €5/month.

1. [Home](https://runsite.app/)
2. [Services](https://runsite.app/services)
3. Redis

Data EU · Germany

# Managed Redis Hosting in the EU

In-memory cache for sessions, queues, and real-time features. Toggle it on and go.

[Start with €5 credit](https://dashboard.runsite.app/login)[Read the docs](https://docs.runsite.app/services/redis/)

Latency

< 1ms

Memory

Up to 10 GB

Version

Redis 7+

redis-cli — runsite

 redis 7

 redis-cli -u $REDIS_URL --tls

 SET session:8f2 "{...}" EX 900

 OK · 0.31 ms

 GET session:8f2

 "{...}" · 0.24 ms

 persistence RDB+AOF · failover auto 

Cached data and session tokens stay in Germany

Overview

Runsite Managed Redis is a one-click in-memory store for caching, sessions, queues, and real-time features. Toggle it on, copy the connection string, and you have a Redis 7+ instance with sub-millisecond latency and TLS on by default.

Instances run inside the European Union, so cached data and user sessions stay within EU borders. A signed GDPR Data Processing Agreement is included on every plan.

You get the full Redis data-structure set, Pub/Sub messaging, optional RDB and AOF persistence, and automatic failover. Most teams run it next to [managed PostgreSQL](https://runsite.app/services/postgresql) to cache queries and take read load off the database, or wire it into their [web services](https://runsite.app/services/web-services) for rate limiting and job queues. Redis starts at €5/mo for 256 MB, which is what the €5 credit on a new account covers if Redis is the only thing you run.

Last reviewed: September 2026

At a glance

Latency

< 1ms

Memory

Up to 10 GB

Version

Redis 7+

Persistence

RDB + AOF

Encryption

TLS default

Failover

Automatic

Hosted in the EU · Germany

[Start with €5 credit](https://dashboard.runsite.app/login)

Features

## Everything You Need from Managed Redis

Built-in tools and capabilities to deploy and manage with confidence.

### Sub-Millisecond Latency

In-memory data access with sub-millisecond response times. Ideal for caching, session storage, and real-time leaderboards.

The number is real and rarely the reason to add a cache. A cache pays for itself when the same key is read repeatedly and the source query is genuinely expensive; if writes outnumber reads, the second copy adds invalidation and memory cost for almost no benefit.

### Pub/Sub Messaging

Built-in publish/subscribe messaging for real-time features, live notifications, and event-driven architectures.

Publish and subscribe with no persistence: a message goes to the subscribers connected at that moment and to nobody else. That makes it right for live notifications and log streaming, and wrong for a job queue, where a subscriber that was restarting must still get the work.

### Persistent Storage

Optional data persistence with RDB snapshots and AOF logging. Your data survives restarts and failovers.

RDB snapshots and AOF logging are both enabled, so an instance survives a restart with its data. That matters most for the things people keep in Redis that exist nowhere else — sessions, queue state, rate-limiter counters — which a pure cache configuration would happily throw away.

### TLS Encryption

All connections encrypted with TLS by default. Private networking available for services within Runsite.

Connections are encrypted by default, and services inside Runsite can also reach the instance over private networking. Neither removes the harder question, which is what the instance holds: sessions and rate-limiter counters are personal data, and they sit in your backups as long as the retention window.

### Automatic Failover

Redis instances are automatically restarted on failure and rescheduled to healthy nodes with minimal downtime.

A failed instance is restarted and rescheduled onto a healthy node. A restart is not free even so: connections drop and have to be re-established, so application code needs a retry around the first call after one rather than treating a cache read as infallible.

### Data Structures

Full Redis data structure support: strings, hashes, lists, sets, sorted sets, streams, and HyperLogLog.

Strings, hashes, lists, sets, sorted sets, streams and HyperLogLog, which is the difference between Redis and a plain key-value cache. Sorted sets are what make a sliding-window rate limiter possible in a few commands; streams are what make a durable queue possible without adding a broker.

Caching Pattern

## How Redis fits your stack

Redis sits between your application and database, reducing latency and offloading read traffic.

1

### App Checks Cache First

Your application queries Redis before hitting the database. Cache hits return data in sub-millisecond time, avoiding expensive database queries entirely.

2

### Cache Miss → Database Query

On a cache miss, your app queries PostgreSQL, stores the result in Redis with a TTL (time-to-live), and returns it to the user. Subsequent requests are served from cache.

3

### Real-Time Features via Pub/Sub

Use Redis Pub/Sub channels for live notifications, chat messages, or presence indicators. Multiple web service instances subscribe to the same channel for consistent real-time behavior.

4

### Session & Queue Management

Store user sessions with automatic expiration across all your service instances. Use Redis lists as lightweight job queues for background processing without a separate queue service.

Code

## Get Started in Minutes

Simple integration with your existing tools and workflows.

connection.sh

```
# Add to your environment variables
REDIS_URL=redis://default:password@redis.runsite.app:6379

# With TLS
REDIS_URL=rediss://default:password@redis.runsite.app:6379
```

Use Cases

## Common Redis Workloads

Common scenarios where this service shines.

### Application Caching

Cache database queries, API responses, and computed results to reduce latency and database load.

### Session Management

Store user sessions with automatic expiration. Share sessions across multiple web service instances.

### Real-Time Features

Build live dashboards, chat applications, presence indicators, and real-time notifications with Pub/Sub.

### Rate Limiting & Queues

Implement API rate limiting, job queues, and task scheduling with Redis lists and sorted sets.

EU-Sovereign

## Your Cached Data Stays in the EU

Hosted in Germany — your data stays under EU jurisdiction.

Your Redis instance, the keys you cache, user sessions, and any RDB or AOF persistence files run in Runsite's German region. Cached personal data and session tokens never leave the EU.

Every account is GDPR-compliant by default and includes a signed Data Processing Agreement at no extra cost and with no sales call. You can review how we handle data in our [Privacy Policy and DPA terms](https://runsite.app/privacy). That makes Runsite a fit when you handle EU customer data or fall under the GDPR or the EU Digital Services Act.

EU data residencyGDPR-readySigned DPA on every plan

Pricing

## Simple, transparent pricing

Pay only for the resources you actually use. Switch between monthly and hourly to see what you'll spend.

Runsite is in Public Beta

Every service listed here is live, and paid plans bill exactly as shown. Beta means the platform is young. Features still change, and parts of the roadmap are not built yet.

### Long-running web servers

·Apps · APIs · Backends

Push your repo, Runsite builds and serves. Auto-scale, zero-downtime rolling deploys, free SSL.

Web Services

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Disk

1 GB

Bandwidth

100 GB

[Choose Nano](https://dashboard.runsite.app/login)

Web Services

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

512 MB

Disk

5 GB

Bandwidth

1 TB

[Choose Starter](https://dashboard.runsite.app/login)

Web Services

### Standard

Popular

€12/mo

≈ €0.0164/hour, billed hourly

vCPU

1

RAM

1 GB

Disk

10 GB

Bandwidth

2 TB

[Choose Standard](https://dashboard.runsite.app/login)

Web Services

### Pro

€25/mo

≈ €0.0342/hour, billed hourly

vCPU

1

RAM

2 GB

Disk

10 GB

Bandwidth

5 TB

[Choose Pro](https://dashboard.runsite.app/login)

Web Services

### Pro Plus

€50/mo

≈ €0.0685/hour, billed hourly

vCPU

2

RAM

4 GB

Disk

20 GB

Bandwidth

10 TB

[Choose Pro Plus](https://dashboard.runsite.app/login)

Web Services

### Business

€95/mo

≈ €0.1301/hour, billed hourly

vCPU

4

RAM

8 GB

Disk

20 GB

Bandwidth

20 TB

[Choose Business](https://dashboard.runsite.app/login)

Web Services

### Business Plus

€180/mo

≈ €0.2466/hour, billed hourly

vCPU

8

RAM

16 GB

Disk

30 GB

Bandwidth

30 TB

[Choose Business Plus](https://dashboard.runsite.app/login)

Nano is the entry plan: the same 0.1 vCPU / 256 MB as before, now €3/mo and covered by your €5 starting credit. Nano services draw on a 100 GB monthly egress pool, ship a noindex header, and sleep after 14 days with no traffic. Starter and above are always-on, indexable and unmetered on egress.

### JAMstack & static frontends

·Landing pages · Docs · Portfolios

Deploy Next.js, Astro, Vite, Hugo and friends to a global CDN. Auto-deploy from Git, preview on every PR, free SSL.

Static Sites

### Free

Popular

Free

Never billed · never draws on your balance

CDN

Global

Bandwidth

100 GB

Builds

\~20s

Sites

Unlimited

[Deploy for free](https://dashboard.runsite.app/login)

Static sites are not billed. No plan, no hourly charge, and they never draw on your balance or credit. Pay-as-you-go bandwidth above the included quota will be introduced after launch. We'll notify you before any change.

### Production-ready databases

·Managed · Backups · EU-sovereign

One-click PostgreSQL with daily backups, connection pooling, encrypted at rest, never leaves the EU.

PostgreSQL

### Nano

€3/mo

≈ €0.0041/hour, billed hourly

vCPU

0.1

RAM

256 MB

Storage

1 GB

Connections

25

[Choose Nano](https://dashboard.runsite.app/login)

PostgreSQL

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

vCPU

0.5

RAM

1 GB

Storage

5 GB

Connections

50

[Choose Starter](https://dashboard.runsite.app/login)

PostgreSQL

### Standard

Popular

€14/mo

≈ €0.0192/hour, billed hourly

vCPU

1

RAM

2 GB

Storage

10 GB

Connections

100

[Choose Standard](https://dashboard.runsite.app/login)

PostgreSQL

### Pro

€35/mo

≈ €0.0479/hour, billed hourly

vCPU

2

RAM

4 GB

Storage

20 GB

Connections

200

[Choose Pro](https://dashboard.runsite.app/login)

PostgreSQL

### Pro Plus

€70/mo

≈ €0.0959/hour, billed hourly

vCPU

4

RAM

8 GB

Storage

50 GB

Connections

400

[Choose Pro Plus](https://dashboard.runsite.app/login)

PostgreSQL

### Business

€130/mo

≈ €0.1781/hour, billed hourly

vCPU

8

RAM

16 GB

Storage

100 GB

Connections

500

[Choose Business](https://dashboard.runsite.app/login)

Storage beyond the included quota: +€0.25/GB/mo. Daily encrypted backups included. Nano is the entry plan at €3/mo. There is no longer a 30-day free database. If your credit and balance run out the database stops with its volume intact; press Start after topping up and it comes straight back. Storage is reclaimed 7 days later and the final backup is kept for 12 months.

### Sub-ms in-memory store

·Cache · Sessions · Queues

Drop-in Redis 7+ compatible store. Pub/Sub, automatic failover, TLS, RDB + AOF persistence.

Redis-compatible

### Starter

€5/mo

≈ €0.0068/hour, billed hourly

Memory

256 MB

Connections

250

[Choose Starter](https://dashboard.runsite.app/login)

Redis-compatible

### Standard

Popular

€13/mo

≈ €0.0178/hour, billed hourly

Memory

1 GB

Connections

1,000

[Choose Standard](https://dashboard.runsite.app/login)

Redis-compatible

### Plus

€25/mo

≈ €0.0342/hour, billed hourly

Memory

2 GB

Connections

2,000

[Choose Plus](https://dashboard.runsite.app/login)

Redis-compatible

### Pro

€65/mo

≈ €0.0890/hour, billed hourly

Memory

5 GB

Connections

5,000

[Choose Pro](https://dashboard.runsite.app/login)

Redis-compatible

### Max

€125/mo

≈ €0.1712/hour, billed hourly

Memory

10 GB

Connections

10,000

[Choose Max](https://dashboard.runsite.app/login)

Redis starts at Starter. The 50 MB free instance is no longer offered. Every plan here includes RDB + AOF persistence, TLS and automatic failover, and is billed by the hour like everything else. A Starter instance runs for roughly a month on the €5 starting credit, if it is the only thing you have running.

Runsite hosts applications, not relays. VPN endpoints, proxies, VLESS / Xray / V2Ray and Shadowsocks nodes, and traffic tunnels of any kind are prohibited on every plan, paid ones included, and are blocked without notice or refund. See the Acceptable Use Policy in Section 4 of the Terms.

### Every plan, side by side

__Redis-compatible plans, with monthly and hourly prices in euro__
| Plan     | Memory | Connections | Per month | Per hour |
| -------- | ------ | ----------- | --------- | -------- |
| Starter  | 256 MB | 250         | €5        | €0.0068  |
| Standard | 1 GB   | 1,000       | €13       | €0.0178  |
| Plus     | 2 GB   | 2,000       | €25       | €0.0342  |
| Pro      | 5 GB   | 5,000       | €65       | €0.089   |
| Max      | 10 GB  | 10,000      | €125      | €0.1712  |

[Compare all plans](https://runsite.app/pricing)[Try the calculator](https://runsite.app/pricing/calculator)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Which Redis version is supported?

Runsite runs Redis 7+ which includes all modern features: Redis Functions, client-side caching, ACLv2, and improved memory efficiency. We upgrade to the latest stable release automatically.

### Is data persisted across restarts?

Redis data is persisted using RDB snapshots and AOF (Append Only File) logging on every plan. Snapshots survive restarts and failovers, so a cache warmed over days is not lost the first time an instance moves.

### Is there a free Redis plan?

No. The 50 MB free instance is no longer offered — Redis starts at Starter, €5/mo for 256 MB, billed by the hour like everything else. The €5 of credit on a new account covers roughly a month of it, if it is the only service you have running. Instances created on the old free plan keep running until their owner deletes them, but the plan cannot be selected for anything new. If your credit and balance run out, the instance stops. Redis is in-memory and is not backed up, so export anything you want to keep before that happens — a Postgres database in the same situation keeps its volume, Redis does not.

### Can I use Redis as a primary database?

While Redis supports persistence, it is optimized for caching and in-memory workloads. For primary data storage, we recommend PostgreSQL. Redis is best used alongside PostgreSQL for caching, sessions, and real-time features.

### How do I connect from my web service?

Add the REDIS\_URL environment variable to your web service. Runsite provides both internal (private network) and external connection strings. Internal connections are faster and free of charge.

Guides

## Learn More About Redis

Hands-on articles from the Runsite blog.

[Caching12 min readWhen to Use Redis as a Cache (and When It Makes Things Worse)A cache is a second copy of your data that is allowed to be wrong. This is the decision frame: what to cache, what never to, the four failures that only appear in production, and why a cache full of sessions is a compliance object.Read](https://runsite.app/blog/when-to-use-redis-cache)[Databases10 min readConnection Pooling with PgBouncer: What Ports 5432 and 6432 Really DoYou switched to port 6432 to stop the connection errors, and now prepared statements throw and SET doesn't stick. That's pooling working as designed. Here's what a pooler actually does, why 5432 and 6432 are different contracts, and what breaks in transaction mode.Read](https://runsite.app/blog/pgbouncer-connection-pooling)[Databases10 min readPostgreSQL "FATAL: sorry, too many clients already" — What It Means and How to Fix ItYour app is suddenly throwing 500s and the database logs are full of "too many clients already." Here's what the error really means, how to see what's hogging your connections, and the fix that actually holds: pooling, not a bigger number.Read](https://runsite.app/blog/postgres-too-many-clients-already)[Deployment7 min readWhat Are Cold Starts — and Why Your Free-Tier App Keeps Falling AsleepCold starts make the first request to a sleeping app painfully slow. Here's why free hosting tiers spin down, what it costs you, and how to keep an app warm without paying for idle capacity.Read](https://runsite.app/blog/cold-starts)

Related

## You Might Also Need

Services that work great together.

[Web ServicesNode.js, Python, Go, Ruby, Rust, or your own Dockerfile — Runsite builds it, runs it, and keeps it online on European infrastructure.Learn more](https://runsite.app/services/web-services)[PostgreSQLManaged PostgreSQL. Daily backups, one-click restore. Just connect and start querying.Learn more](https://runsite.app/services/postgresql)[Cron JobsRun scheduled tasks automatically. Set a schedule, we run your code — with logs and retries built in.Learn more](https://runsite.app/services/cron-jobs)

---

# S3 Object Storage — S3-Compatible Storage on Runsite

> S3-compatible object storage in the EU with zero egress fees. Works with the AWS SDK, boto3 and any S3 client. 5 GB free, then €0.025 per GB per month.

1. [Home](https://runsite.app/)
2. [Services](https://runsite.app/services)
3. S3 Object Storage

Storage EU · Germany

# EU S3-Compatible Object Storage, Zero Egress Fees

Store files, images, and backups. S3-compatible — use the tools you already know.

[Start with €5 credit](https://dashboard.runsite.app/login)[Read the docs](https://docs.runsite.app/services/object-storage/)

Storage

Unlimited

Object Size

Up to 5 TB

API

S3-compatible

aws-cli — runsite

 s3 api

 aws s3 cp ./media s3://assets \

 --endpoint-url https://s3.runsite.app

 1,284 objects · 4.2 GB uploaded

 runsite storage bill --month

 storage  4.2 GB × €0.025      €0.11

 egress   812 GB               €0.00 

AES-256 at rest · no replication outside the EU

Overview

Runsite Object Storage is S3-compatible storage for files, images, videos, and backups. It works with the AWS SDK, boto3, and any S3 client: point the endpoint at Runsite, use your access keys, and your existing code runs unchanged.

Egress is free. There are no charges for downloads, uploads, or data transfer of any kind, so you pay only for the gigabytes you store, billed at €0.025 per GB each month after a 5 GB free tier. That removes the bandwidth bill that makes serving images and media expensive on metered object storage.

All objects sit inside the European Union, encrypted at rest with AES-256, with a signed GDPR Data Processing Agreement included. Public buckets are served through a global CDN, while private files use time-limited presigned URLs so you can share access without handing out credentials. It is a natural home for [PostgreSQL](https://runsite.app/services/postgresql) backups, user uploads from your [web services](https://runsite.app/services/web-services), and build assets from your [static sites](https://runsite.app/services/static-sites).

Last reviewed: September 2026

At a glance

Storage

Unlimited

Object Size

Up to 5 TB

API

S3-compatible

Encryption

AES-256 at rest

CDN

Global edge

Access

Keys + presigned

Hosted in the EU · Germany

[Start with €5 credit](https://dashboard.runsite.app/login)

Features

## Everything You Need from Object Storage

Built-in tools and capabilities to deploy and manage with confidence.

### Bucket Management

Create and manage storage buckets with public or private access. Organize files with folder-like prefixes and metadata tags.

Buckets are flat key spaces, not directories: a prefix that looks like a folder is part of the key. That is why renaming an object means copying it under a new key and deleting the old one, and why the key should be generated server-side and kept opaque.

### Access Keys & Permissions

Generate access keys with granular permissions: read, write, delete, or full admin. Rotate keys without downtime.

Keys carry read, write, delete or admin scope and rotate without downtime. Moving from a cloud provider's role-based identity to a static key pair is a real change in your threat model, so keep keys server-side and out of anything a browser can read.

### Presigned URLs

Generate time-limited presigned URLs for secure file sharing and direct client-side uploads without exposing credentials.

A presigned URL is a request signed with your key, with the signature moved into the query string. Minting one is free and offline, it can never grant more than the key behind it, and revoking one means rotating that key — so keep the expiry short.

### Usage Monitoring

Track storage usage, bandwidth consumption, and request counts in real time. Set alerts for usage thresholds.

Storage, bandwidth and request counts in real time, with thresholds you can alert on. Requests are the line worth watching: a workload that lists a bucket on every page view can cost more in operations than in the gigabytes it is storing.

### CDN Integration

Serve public bucket files through Runsite's global CDN for fast asset delivery. Automatic cache headers for static content.

Public buckets are served through the CDN with cache headers applied, which is what makes serving images and media from storage practical. Egress is free at every size here, so the bandwidth line that usually makes this expensive is simply absent from the invoice.

### Encryption at Rest

All objects encrypted at rest using AES-256\. TLS encryption for all data in transit. CORS configuration support.

Objects are encrypted with AES-256 at rest and TLS in transit, and CORS is configurable per bucket so a browser can upload directly. Everything stays in Germany, which means the storage layer gives the same answer to a security review as the rest of the stack.

Integration Guide

## Connect with any S3 SDK

Runsite's S3-compatible API works with existing AWS SDKs and tools — no code changes needed.

1

### Create a Bucket & Access Key

Create a storage bucket from the dashboard with public or private access. Generate an access key pair with read, write, or admin permissions.

2

### Point Your SDK to Runsite

Use the AWS SDK, boto3, or any S3-compatible client. Set the endpoint URL to your Runsite storage endpoint and use your Runsite access keys — no other configuration needed.

3

### Upload & Organize Files

Upload files using standard S3 PutObject calls. Organize with folder-like prefixes (e.g. uploads/2026/photo.jpg). Use multipart upload for files over 100 MB.

4

### Serve via CDN or Presigned URLs

Public buckets are served through Runsite's global CDN automatically. For private files, generate time-limited presigned URLs for secure sharing without exposing credentials.

Code

## Get Started in Minutes

Simple integration with your existing tools and workflows.

aws-sdk-(node.js).js

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

const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY,
    secretAccessKey: process.env.S3_SECRET_KEY,
  },
});

await s3.send(new PutObjectCommand({
  Bucket: 'my-bucket',
  Key: 'uploads/photo.jpg',
  Body: fileBuffer,
}));
```

Use Cases

## What You Can Store

Common scenarios where this service shines.

### Media Storage

Store user-uploaded images, videos, and documents with automatic content-type detection and CDN delivery.

### Database Backups

Store PostgreSQL and Redis backups in object storage with lifecycle policies for automatic cleanup.

### Static Assets

Serve JavaScript bundles, CSS files, and fonts from S3 with CDN caching for fast global delivery.

### Data Archival

Archive logs, analytics data, and compliance records with cost-effective long-term storage.

EU-Sovereign

## EU Object Storage, GDPR-Ready

Hosted in Germany — your data stays under EU jurisdiction.

Every object you upload is stored in Runsite's German region and encrypted at rest with AES-256\. Files are not replicated to non-EU regions, so media, user uploads, and backups stay inside EU borders by default.

Every account is GDPR-compliant by default and includes a signed Data Processing Agreement at no extra cost and with no sales call. You can review how we handle data in our [Privacy Policy and DPA terms](https://runsite.app/privacy). That makes Runsite a fit when you handle EU customer data or fall under the GDPR or the EU Digital Services Act.

EU data residencyGDPR-readySigned DPA on every plan

Pricing

## Simple, transparent pricing

Pay only for the resources you actually use. Switch between monthly and hourly to see what you'll spend.

Runsite is in Public Beta

Every service listed here is live, and paid plans bill exactly as shown. Beta means the platform is young. Features still change, and parts of the roadmap are not built yet.

### Free

Free

* 5 GB included
* Unlimited operations
* Zero egress fees
* S3-compatible API
[Deploy for free](https://dashboard.runsite.app/login)

### Pay-as-you-go

€0.025/GB/mo

* Pay only for what you use
* 100 GB ≈ €2.50/mo
* 500 GB ≈ €12.50/mo
* 1 TB ≈ €25/mo · 5 TB ≈ €125/mo
[Get Started](https://dashboard.runsite.app/login)

### Every plan, side by side

__S3 Object Storage plans and what each includes__
| Plan          | Price        | What it includes                                                                                 |
| ------------- | ------------ | ------------------------------------------------------------------------------------------------ |
| Free          | Free         | 5 GB included. Unlimited operations. Zero egress fees. S3-compatible API                         |
| Pay-as-you-go | €0.025/GB/mo | Pay only for what you use. 100 GB ≈ €2.50/mo. 500 GB ≈ €12.50/mo. 1 TB ≈ €25/mo · 5 TB ≈ €125/mo |

[Compare all plans](https://runsite.app/pricing)[Try the calculator](https://runsite.app/pricing/calculator)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is the storage API compatible with AWS S3?

Yes. Runsite's object storage is fully S3-compatible. You can use the AWS SDK, boto3, aws-cli, or any S3-compatible client library. Just point the endpoint to your Runsite storage URL and use your Runsite access keys.

### What is the maximum file size?

Individual objects can be up to 5 TB in size. For files larger than 100 MB, we recommend using multipart uploads for better reliability and performance. The AWS SDK handles multipart uploads automatically.

### Can I serve files publicly?

Yes. Set a bucket to public access and files are served directly via URL. Public buckets are automatically integrated with Runsite's CDN for fast global delivery. Private buckets require presigned URLs or access key authentication.

### How is bandwidth billed?

It isn't. Egress is free — there are no charges for downloads, uploads, or data transfer of any kind. You pay only for the storage you use; bandwidth is never metered or billed.

Guides

## Learn More About S3 Object Storage

Hands-on articles from the Runsite blog.

[Storage11 min readS3-Compatible Object Storage: How It Works and How to Choose a Provider"S3" names three separate things: a product, a storage model, and an API. This guide separates them — how buckets actually behave, what "S3-compatible" guarantees, where that compatibility runs out, and the axes worth comparing providers on.Read](https://runsite.app/blog/s3-compatible-object-storage)[Storage13 min readPresigned URLs: Share and Upload Private S3 Files Without Handing Out Your KeysA presigned URL isn't a token the storage issued you. It's a signature you computed yourself, and almost everything people get wrong about expiry, revocation, and safety follows from that. Here's the mechanism, what one looks like, and how to use it for both downloads and uploads.Read](https://runsite.app/blog/s3-presigned-urls)[Storage11 min readUploading User Files to S3: Keys, Multipart, and What to RetryUploading a file is three lines of SDK code, and they stay correct until a real user shows up with a 3 GB video on hotel wifi. Here's what comes after the tutorial: naming objects you can't rename, splitting large files, and knowing which failures are safe to repeat.Read](https://runsite.app/blog/upload-files-to-s3)[Storage8 min readS3 Egress Fees: Why Downloading Your Own Data Costs So MuchStorage is the cheap line on an object-storage bill. Egress, the charge for data leaving the bucket, is where the money actually goes. Here's how S3 data-transfer fees work, how to calculate your real bill, and why zero-egress storage changes the math.Read](https://runsite.app/blog/s3-egress-fees)[Storage11 min readAWS S3 Alternatives Without Egress Fees: What to Check Before You SwitchZero-egress storage moves the meter, it doesn't remove it. What providers bill instead of transfer, why the saving only appears when bytes leave from the bucket, and which workloads actually get cheaper by leaving S3.Read](https://runsite.app/blog/s3-alternatives-no-egress-fees)[Databases7 min readHow to Automate PostgreSQL Backups to S3 on a ScheduleA managed database backs itself up daily — but a second, portable copy in your own S3 bucket is yours to keep. Here's how to schedule pg\_dump to object storage with a cron job, and when it's worth doing.Read](https://runsite.app/blog/postgresql-backup-to-s3)

Related

## You Might Also Need

Services that work great together.

[Web ServicesNode.js, Python, Go, Ruby, Rust, or your own Dockerfile — Runsite builds it, runs it, and keeps it online on European infrastructure.Learn more](https://runsite.app/services/web-services)[PostgreSQLManaged PostgreSQL. Daily backups, one-click restore. Just connect and start querying.Learn more](https://runsite.app/services/postgresql)[Static SitesNext.js, Astro, Hugo, Vite — any generator that writes static files. Automatic builds, a global CDN, free SSL, and your own domain.Learn more](https://runsite.app/services/static-sites)

---

# Email Service — Transactional Email API on Runsite

> Transactional email from EU infrastructure: receipts, password resets and alerts, with SPF, DKIM and DMARC handled. 3,000 messages a month free, then €1 per 1,000.

1. [Home](https://runsite.app/)
2. [Services](https://runsite.app/services)
3. Email Service

Messaging EU · Germany

# EU Transactional Email API

Send receipts, password resets, and notifications. 3,000 emails included every month.

[Start with €5 credit](https://dashboard.runsite.app/login)[Read the docs](https://docs.runsite.app/services/email/)

Included Quota

3,000/mo

Rate

€1 / 1,000

API

REST + SMTP

curl — mail api

 202

 curl -X POST api.runsite.app/v1/emails \

 -d '{"to":"ada@acme.eu","tpl":"reset"}'

 202 Accepted · id=msg_7f3a91

 webhook  delivered   1.4s

 webhook  opened      00:02:11

 3,000 emails / month included 

Queued in Germany, sent from France, never outside the EU

Overview

Runsite Email is a transactional email API for the messages your app has to send: receipts, password resets, magic links, and alerts. Call the REST API or send over SMTP, and Runsite signs, queues, and delivers each one, then reports delivery, opens, clicks, bounces, and complaints back through webhooks.

The sending infrastructure runs entirely inside the European Union. Messages are processed and logged in the EU under GDPR-friendly retention, with a signed Data Processing Agreement on every plan. That matters when authentication and billing mail carries personal data.

It is built for transactional traffic only, not marketing or newsletters, so your messages never share sender reputation with bulk campaigns. Trigger it from your [web services](https://runsite.app/services/web-services) on signup or checkout, or send scheduled digests from a [cron job](https://runsite.app/services/cron-jobs). Every account includes 3,000 emails a month, then €1 per additional 1,000 with no monthly minimum. Sending to external recipients requires a one-time top-up of at least €5; before that, mail stays inside the sandbox.

Last reviewed: September 2026

At a glance

Included Quota

3,000/mo

Rate

€1 / 1,000

API

REST + SMTP

Region

EU only

Templates

HTML + Text

Webhooks

Opens · Clicks

Hosted in the EU · Germany

[Start with €5 credit](https://dashboard.runsite.app/login)

Features

## Everything to Send Transactional Mail

Built-in tools and capabilities to deploy and manage with confidence.

### Transactional-Only Sending

Built strictly for app-driven mail — receipts, password resets, magic links, alerts. No marketing, no newsletters, no shared-IP penalties.

Marketing mail and application mail share sender reputation when they share an IP, and the marketing volume is what damages it. Keeping campaigns off the platform is why a password reset arrives: the messages a user is actively waiting for are not competing with a newsletter for inbox placement.

### Templates with Variables

HTML and plain-text templates with placeholders. Render server-side via the API call — no template engine on your side.

HTML and plain-text bodies with placeholders, rendered on the server when you call the API. That keeps the template out of your deployment, so changing the wording of a receipt does not require a release, and it keeps both parts of a multipart message in step.

### Custom Sender Domains

Send from your own domain. DKIM, SPF, and DMARC are generated for you — paste two DNS records and you're verified.

SPF, DKIM and DMARC records are generated for you and verified once the DNS entries resolve. All three matter: SPF authorises the sending host, DKIM signs the message so it survives forwarding, and DMARC tells receivers what to do when either check fails.

### Webhooks for Every Event

Real-time webhooks for delivered, opened, clicked, bounced, and complained events. Reconcile state without polling.

Delivered, opened, clicked, bounced and complained events arrive as they happen, so your own records stay correct without polling. Bounces and complaints are the two worth acting on automatically: leaving a hard-bouncing address on the list is what turns a deliverability problem into a reputation one.

### Delivery Analytics

Per-message status and per-domain delivery, open, and click rates. Spot deliverability issues before users start complaining.

Per-message status plus delivery, open and click rates broken out by recipient domain. The per-domain view is the useful one, because deliverability rarely fails everywhere at once — it fails at one large provider first, and an aggregate number hides that until users start reporting it.

### EU-Sovereign by Default

Mail is processed and logged inside the EU. GDPR-friendly retention and a signed DPA included on every plan.

Messages are accepted, signed and queued in Germany and sent by our mail subprocessor in France, with nothing routed through non-EU infrastructure. Bodies are kept for seven days for delivery troubleshooting and then purged; headers and event logs are kept for thirty. A signed GDPR data processing agreement is included on every plan.

Sending Pipeline

## From API call to inbox

Each send goes through verification, signing, queueing, and delivery — with webhooks at every step.

1

### Verify Your Sender Domain

Add your domain in the dashboard. Runsite generates DKIM, SPF, and DMARC records — paste them into your DNS and the domain is verified within minutes.

2

### Queue & Sign the Message

Your API call hits the EU edge, the message is signed with your DKIM key, and it's enqueued for delivery. The API returns a message ID immediately.

3

### Deliver to the Recipient MTA

Runsite negotiates TLS with the recipient mail server, retries on transient failures, and respects per-domain throttling to keep your reputation clean.

4

### Track Events via Webhooks

Delivered, opened, clicked, bounced, and complained events are pushed to your webhook endpoint in real time — no polling, no log scraping.

Code

## Get Started in Minutes

Simple integration with your existing tools and workflows.

curl.sh

```
curl https://api.runsite.app/email/send \
  -H "Authorization: Bearer $RUNSITE_EMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourapp.com",
    "to": "user@example.com",
    "subject": "Welcome to YourApp",
    "html": "<h1>Hi there</h1><p>Thanks for signing up.</p>"
  }'
```

Use Cases

## Transactional Mail You Can Send

Common scenarios where this service shines.

### Welcome & Onboarding

Send welcome emails, verification links, and onboarding nudges as new users sign up — without piping through a marketing tool.

### Password Resets & Magic Links

Time-sensitive auth mail that has to land in the primary inbox. Dedicated transactional infrastructure keeps deliverability tight.

### Receipts & Invoices

Order confirmations, payment receipts, and PDF invoices triggered straight from your checkout or billing webhooks.

### Alerts & Notifications

System alerts, comment replies, and threshold notifications. Webhooks let you reconcile delivery state with your own database.

EU-Sovereign

## Your Mail Is Processed in the EU

Hosted in Germany — your data stays under EU jurisdiction.

Messages are accepted, signed, queued, and delivered from Runsite's German region, and nothing is routed through non-EU infrastructure. Message bodies are kept for 7 days for delivery troubleshooting, then purged; headers and event logs are retained for 30 days — all inside the EU.

Every account is GDPR-compliant by default and includes a signed Data Processing Agreement at no extra cost and with no sales call. You can review how we handle data in our [Privacy Policy and DPA terms](https://runsite.app/privacy). That makes Runsite a fit when you handle EU customer data or fall under the GDPR or the EU Digital Services Act.

EU data residencyGDPR-readySigned DPA on every plan

Pricing

## Simple, transparent pricing

Pay only for the resources you actually use. Switch between monthly and hourly to see what you'll spend.

Runsite is in Public Beta

Every service listed here is live, and paid plans bill exactly as shown. Beta means the platform is young. Features still change, and parts of the roadmap are not built yet.

### Included

€0

* 3,000 emails / month
* Resets at start of each month
* Custom sender domain
* Templates, webhooks, analytics
* Sending to external recipients needs a €5 top-up
[Get Started](https://dashboard.runsite.app/login)

### Pay-as-you-go

€1 / 1,000

* Charged only above 3,000/mo
* 5,000 emails ≈ €2
* 10,000 emails ≈ €7
* 50,000 emails ≈ €47
[Get Started](https://dashboard.runsite.app/login)

### Every plan, side by side

__Email Service plans and what each includes__
| Plan          | Price      | What it includes                                                                                                                                            |
| ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Included      | €0         | 3,000 emails / month. Resets at start of each month. Custom sender domain. Templates, webhooks, analytics. Sending to external recipients needs a €5 top-up |
| Pay-as-you-go | €1 / 1,000 | Charged only above 3,000/mo. 5,000 emails ≈ €2\. 10,000 emails ≈ €7\. 50,000 emails ≈ €47                                                                   |

[Compare all plans](https://runsite.app/pricing)[Try the calculator](https://runsite.app/pricing/calculator)

FAQ

## Frequently Asked Questions

Common questions about this service.

### How is sending billed?

Every account gets 3,000 emails per calendar month at no charge. Above that, you pay €1 per additional 1,000 emails — pro-rated, no rounding tricks. The included quota resets on the 1st of each month and there is no monthly minimum or subscription. Production sending is gated separately: until the account has topped up at least €5, mail is restricted to the sandbox — your own verified addresses only. The €5 starting credit does not lift that gate, because it is a credit and not a payment. One top-up of €5 unlocks it permanently.

### Can I send marketing or newsletter campaigns?

No. Runsite Email is strictly for transactional traffic — receipts, password resets, magic links, and alerts triggered by user actions. Bulk marketing, cold outreach, and unsolicited mail violate the terms and will get the account suspended. Use a dedicated marketing platform for those.

### How do I send from my own domain?

Add your domain in the dashboard. Runsite generates DKIM, SPF, and DMARC records you copy into your DNS provider. Verification usually completes in a few minutes. Until you verify, sends fall back to a runsite.email subdomain so testing isn't blocked.

### Are there rate limits?

New accounts send up to 10 messages per second. The rate scales automatically as your sending reputation builds. If you need a higher burst rate or dedicated IPs, contact support — we'll provision a sending pool that fits your volume.

### Where is mail processed and stored?

All sending infrastructure runs inside the EU. Message bodies are kept for 7 days for delivery troubleshooting, then purged. Headers and event logs are retained for 30 days. A signed DPA is included on every account, no negotiation required.

Guides

## Learn More About Email Service

Hands-on articles from the Runsite blog.

[Compliance10 min readData Processing Agreement for SaaS: What Article 28 RequiresA deal stalls because their legal team wants your DPA before signing. Here's what GDPR Article 28 requires inside a Data Processing Agreement, which side of it your SaaS is on, and how to keep it from becoming a bottleneck in your sales cycle.Read](https://runsite.app/blog/what-is-a-data-processing-agreement-dpa)[Compliance9 min readWhere to Store EU User Data: What GDPR Actually RequiresA customer's security review asks where their data physically lives and whether it leaves the EU. GDPR has no hard rule that personal data must stay in Europe — but every transfer out needs a legal mechanism, and EU residency is the simplest way to skip that problem entirely.Read](https://runsite.app/blog/gdpr-data-residency)[Compliance11 min readEU Hosting for Developers: Data Residency, GDPR, and DPAs in PracticeA customer asks where their data lives, or a deal stalls on a DPA request, and suddenly "we host in Europe" isn't a good enough answer. Here's what EU hosting actually means for developers: data residency, GDPR roles, DPAs, and how to build a stack that keeps data on EU soil.Read](https://runsite.app/blog/eu-hosting-for-developers)[Compliance10 min readSchrems II and Data Transfers: Why an EU-Only Stack Is the Stable AnswerSchrems II struck down Privacy Shield in 2020 — the second EU-US data bridge to collapse in five years. Here's what the ruling actually said, why these frameworks keep failing, and why keeping data in the EU sidesteps the entire cycle.Read](https://runsite.app/blog/schrems-ii-data-transfers)

Related

## You Might Also Need

Services that work great together.

[Web ServicesNode.js, Python, Go, Ruby, Rust, or your own Dockerfile — Runsite builds it, runs it, and keeps it online on European infrastructure.Learn more](https://runsite.app/services/web-services)[PostgreSQLManaged PostgreSQL. Daily backups, one-click restore. Just connect and start querying.Learn more](https://runsite.app/services/postgresql)[Cron JobsRun scheduled tasks automatically. Set a schedule, we run your code — with logs and retries built in.Learn more](https://runsite.app/services/cron-jobs)

---

> Tools for working with Runsite outside the dashboard: the runsite command-line interface for your terminal and CI, and an MCP server for AI coding agents.

# Runsite outside the dashboard

The dashboard is one way to run your services. The CLI and the MCP server reach the same API from a terminal, a build pipeline or an AI agent.

[Install the CLI](https://runsite.app/tools/cli)[Connect an agent](https://runsite.app/tools/mcp)

bash

 git push origin main

 runsite deploy api

 building 8e4c1a2 on main .......... 24.1s

 health check /healthz ........... 200 OK

 released in 38s

Start the deploy wherever you are. The release comes out the same.

[ Command linerunsite v0.1.0Deploy a service, read its logs and change its environment from your shell. It is one binary for macOS, Linux and Windows, and your build pipeline runs the same one.Runs onmacOS, Linux, Windows (5 builds)Signs in withrunsite login, or RUNSITE\_API\_TOKEN in CIOutput\--output json on anything that prints dataInstallChecked against the release's SHA‑256 sums runsite deploy apiInstall and use it](https://runsite.app/tools/cli)[ MCP servermcp v0.1.0Point Claude Code, Cursor or another MCP client at one hosted endpoint. The agent can look at your services, follow a failing build and deploy the fix, with a key whose scope you choose.Runs onOur servers, so there is nothing to installOffers22 tools, filtered by the key's scopeLimit60 tool calls a minute per keyNever returnsEnv values, secrets or connection URLshttps://mcp.runsite.app/mcpConnect a client](https://runsite.app/tools/mcp)

## Which one for which job

Most everyday jobs work from all three. The table gives the command or tool name for each one and shows where you still need the dashboard.

| Job                         | Dashboard     | CLI                                       | MCP server                                  |
| --------------------------- | ------------- | ----------------------------------------- | ------------------------------------------- |
| Deploy the latest commit    | Yes           | runsite deploy api                        | deploy                                      |
| Read recent logs            | Yes           | runsite logs api                          | get\_logs                                   |
| Restart or stop a service   | Yes           | runsite service restart api               | restart, stop                               |
| Set an environment variable | Yes           | runsite env set api KEY=value             | set\_envYou type the value in the dashboard |
| Find out why a build failed | Yes           | Not available                             | get\_deployment                             |
| Roll back a deployment      | Yes           | Not available                             | rollbackYou approve it in the dashboard     |
| Deploy from a CI job        | Not available | RUNSITE\_API\_TOKENNo interactive sign-in | Not available                               |
| Open a shell in a container | Yes           | Not yet                                   | Not available                               |

## One API key works for both

Create a key under Settings → API keys and give it a scope. Read lets it look around, write lets it deploy and restart, and admin adds requests for secrets and deletions. Revoke the key in the dashboard and both tools lose access on their next call.

[Create an API key](https://dashboard.runsite.app/settings/api-keys)

`ak_live_…`readwriteadmin

 The CLI reads it from the environment

` export RUNSITE_API_TOKEN=ak_live_…`

 An MCP client sends it with every call

`Authorization: Bearer ak_live_…`

---

# Runsite CLI — deploy from your terminal

> Deploy services, read logs and set environment variables on Runsite from your terminal. Install the runsite CLI on macOS, Linux or Windows, or run it in CI with an API key.

1. [Home](https://runsite.app/)
2. [Tools](https://runsite.app/tools)
3. CLI

runsite v0.1.0

# Deploy without leaving the terminal

A single binary that talks to the same API as the dashboard. Ship a service, read its logs, change an environment variable — from your shell or from a build pipeline.

`curl -fsSL https://raw.githubusercontent.com/runsite-platform/runsite-cli/main/install/install.sh | sh`

Installs to \~/.local/bin/runsite. Add that directory to your PATH if the installer says it is missing.

[Read the docs](https://docs.runsite.app/cli/overview)[Source and releases](https://github.com/runsite-platform/runsite-cli)

bash — runsite

 v0.1.0

 runsite deploy api

 building 8e4c1a2 on main .......... 24.1s

 health check /healthz ........... 200 OK

 released in 38s

 runsite logs api --tail 3

 INFO  listening on 0.0.0.0:8080

 INFO  migrations up to date

 INFO  GET /healthz 200 1.4ms 

Same API as the dashboard, running in Germany

## What you can do with it

Name a service to act on it, or leave the name out when there is only one — the CLI resolves it for you. Every command that prints data also accepts `--output json`.

### Sign in

One API key per machine, stored in your config file.

`runsite login`

Sign in with email and password, mint a key for this machine

`runsite login --token ak_live_…`

Use a key you created in the dashboard

`runsite whoami`

Show the account the current key belongs to

`runsite logout`

Remove the key from this machine

### Run services

The same start, stop and restart the dashboard performs.

`runsite service list`

Every service, its status and its URL

`runsite service status api`

Branch, instances and last change for one service

`runsite service start api`

Bring a stopped service back up

`runsite service stop api`

Stop a service and stop billing it

`runsite service restart api`

Roll the running containers

### Ship changes

Deploy the current branch and read what the container printed.

`runsite deploy api`

Build and release the latest commit

`runsite logs api --tail 200`

Recent container output

### Configure

Environment variables take effect on the next deploy or restart.

`runsite env list api`

Every variable set on a service

`runsite env set api LOG_LEVEL=debug`

Add or update one or more variables

`runsite env delete api LOG_LEVEL`

Remove variables

### Move between projects

Profiles keep separate accounts and environments apart.

`runsite project list`

Projects on the account

`runsite project use billing`

Resolve service names inside one project

`runsite context show`

Active profile, API URL and project

`runsite completions zsh`

Shell completions for bash, zsh or fish

## Install it your way

The installers pick the right binary for your machine and check it against the release's SHA-256 manifest before unpacking it. Prefer to do that by hand? Grab an archive from the releases page.

| Platform | Architecture  | Archive                                           |
| -------- | ------------- | ------------------------------------------------- |
| Linux    | x86\_64       | runsite-v0.1.0\-x86\_64-unknown-linux-musl.tar.gz |
| Linux    | ARM64         | runsite-v0.1.0\-aarch64-unknown-linux-musl.tar.gz |
| macOS    | Intel         | runsite-v0.1.0\-x86\_64-apple-darwin.tar.gz       |
| macOS    | Apple silicon | runsite-v0.1.0\-aarch64-apple-darwin.tar.gz       |
| Windows  | x86\_64       | runsite-v0.1.0\-x86\_64-pc-windows-msvc.zip       |

Pin a version with `RUNSITE_VERSION=v0.1.0` or change the target directory with `RUNSITE_INSTALL_DIR`. Every archive is listed on the [releases page](https://github.com/runsite-platform/runsite-cli/releases).

## The same binary in CI

Create an API key in the dashboard, store it as a pipeline secret, and set `RUNSITE_API_TOKEN`. No interactive sign-in, no browser step. The key carries the permissions you gave it and you can revoke it at any time.

.github/workflows/deploy.yml

```
- name: Deploy to Runsite
  env:
    RUNSITE_API_TOKEN: ${{ secrets.RUNSITE_API_TOKEN }}
  run: |
    curl -fsSL https://raw.githubusercontent.com/runsite-platform/runsite-cli/main/install/install.sh | sh
    ~/.local/bin/runsite deploy api
```

FAQ

## Frequently Asked Questions

Common questions about this service.

### Do I need a password to use the CLI?

No. If you sign in to Runsite with Google or GitHub, create an API key under Settings → API keys in the dashboard and run \`runsite login --token ak\_live\_…\`. Accounts with a password can run plain \`runsite login\`, which mints a key for that machine automatically.

### How do I use the CLI in CI?

Set RUNSITE\_API\_TOKEN to an API key in your pipeline secrets and skip \`runsite login\` entirely. Every command reads that variable in preference to the stored key, so the same binary works unchanged on a laptop and on a build runner.

### Where does the CLI store my credentials?

In \~/.config/runsite/config.toml on macOS and Linux, and %APPDATA%\\runsite\\config.toml on Windows. The file holds one API key per profile. \`runsite logout\` deletes the local copy; the key itself stays valid until you revoke it in the dashboard.

### Can I manage more than one account?

Yes. Every command accepts \`--profile <name>\`, or reads RUNSITE\_PROFILE. Each profile keeps its own API URL, key and selected project, so a personal account and a work account can live side by side.

### Is the download verified?

Both installers fetch the release's SHA256SUMS.txt and refuse to install a binary whose digest does not match. You can also download an archive from the releases page and check it yourself with sha256sum.

### Can I open a shell in a container from the CLI?

Not yet. \`runsite shell\`, \`runsite run\` and streaming build logs need a live WebSocket connection, which the public API does not expose to API keys at the moment. Use the dashboard for an interactive shell and for watching a build as it happens.

---

# Runsite MCP server — let your agent deploy

> Connect Claude Code, Cursor or another MCP client to Runsite. Your agent gets 22 tools to read logs and deploy, limited to the scope of the API key you give it.

1. [Home](https://runsite.app/)
2. [Tools](https://runsite.app/tools)
3. MCP server

MCP v0.1.0 · 22 tools

# Your agent can deploy what it just wrote

Point an MCP client at one hosted endpoint and it can read your services, follow a failing build to the line that broke it and ship the fix. You don't install or run anything. The agent works with an API key you scope yourself, and secrets stay in the dashboard.

`https://mcp.runsite.app/mcp`

Authorization: Bearer ak\_live\_…

[Read the docs](https://docs.runsite.app/mcp/overview)[Create an API key](https://dashboard.runsite.app/settings/api-keys)

agent — runsite mcp

 22 tools

 get_service_health api

 degraded · last deploy failed 4m ago

 get_deployment api dp_5c1e

 build log: missing STRIPE_SECRET_KEY

 set_env api ["STRIPE_SECRET_KEY"]

 approval ap_8f21 — you enter the value

 deploy api wait=true

 running · 2 instances · healthy 

Every call is made as the owner of the API key you gave it — nothing wider

## Point a client at it

Create a key in the dashboard, drop it into the client you already use, and the tools appear in its next session. The server speaks Streamable HTTP and keeps no state between calls.

terminal

```
claude mcp add --transport http runsite \
  https://mcp.runsite.app/mcp \
  --header "Authorization: Bearer $RUNSITE_API_TOKEN"
```

Reads the key from your shell, so it never lands in a config file. Run \`/mcp\` in Claude Code to confirm the connection.

## What the agent can reach

Every tool takes a service name or its id, and acts as the owner of the key you handed over — never wider. The scope beside each name is what a key needs before the tool appears in its list at all.

### Look around

What exists on the account, and how it is configured.

`whoami`read

The account this key belongs to and what it may do

`list_projects`read

Projects on the account

`list_services`read

Every web service with its status, plan and public URL

`get_service`read

Repository, branch, instances and status of one service

`list_env`read

Variable names on a service — values are always masked

`list_databases`read

Managed Postgres and Valkey in the workspace

`get_database`read

One database and its usage; connection URLs are never returned

### Find out what broke

Enough context to diagnose a service without opening the dashboard.

`get_service_health`read

Status, resource usage, last deployment and error lines in one call

`get_logs`read

Recent container output, capped at 200 lines and 8 KB

`get_metrics`read

CPU, memory and instance count, with the trend over a period

`list_deployments`read

Recent deployments with status, commit and duration

`get_deployment`read

One deployment, plus the build log tail when it failed

### Ship the fix

The same lifecycle actions the dashboard performs.

`deploy`write

Build and release the current commit; wait=true polls until it finishes

`restart`write

Roll the running containers without rebuilding

`start`write

Start a stopped service from its existing image

`stop`write

Stop a service and release its compute; env and disks are kept

`create_service`write

Create a service from a repository on a connected git provider

### Ask you first

Nothing here changes anything until you answer in the dashboard.

`set_env`write

Request environment variables by name; you type the values

`set_secret`admin

The same request for project secrets

`rollback`write

Request a return to an earlier deployment

`delete_service`admin

Request a deletion, which cannot be undone once approved

`check_approval`read

Whether you approved, rejected or let the request expire

## Hand over as little as you like

A key carries one scope, and the server filters the tool list to match it before the agent ever sees it. Start with a read key while you get a feel for what an agent does with your account, and revoke it in the dashboard the moment you want it to stop.

| Scope | Tools    | What it allows                                                                         |
| ----- | -------- | -------------------------------------------------------------------------------------- |
| read  | 13 of 22 | Inspect services, logs, metrics, deployments and databases. Nothing changes.           |
| write | 20 of 22 | Everything above, plus deploy, restart, stop, create a service and request a rollback. |
| admin | 22 of 22 | Everything above, plus requesting project secrets and the deletion of a service.       |

An agent that loops gets 60 tool calls per minute per key and a refusal after that, so a bad retry loop costs you a minute rather than a bill.

## Secrets stop at the dashboard

An agent can tell you a service needs a Stripe key. It cannot be the one holding it. Anything that sets a value, rolls a service back or deletes it goes through the same three-step handshake.

1. 1  
### The agent asks  
It names the keys it needs set. The values are not part of the request, so there is nothing for the agent to guess or leak.  
`set_env("api", ["STRIPE_SECRET_KEY"])`
2. 2  
### You answer in the dashboard  
The tool returns a link and an approval id. You type the values in the dashboard and approve, or reject. Unanswered requests expire.  
`dashboard.runsite.app/approvals`
3. 3  
### The agent reads the outcome  
Polling returns pending, approved, rejected or expired — and only that. The value you entered is never sent back to the agent.  
`check_approval("ap_8f21") → approved`

The rest of the surface follows the same rule: `list_env` masks values, `get_database` never returns a connection URL, and logs come back capped at 200 lines so a transcript cannot quietly swallow your whole output.

## Resources and prompts

Clients that prefer to attach context rather than call a tool can read the same data as a resource. The two prompts are the routines worth having written down: diagnosing a service, and shipping a change without declaring victory too early.

### Resources

`runsite://services`

Every web service on the account as JSON, for clients that read resources instead of calling tools.

`runsite://service/{id}`

One service as JSON, the same shape the dashboard reads.

### Prompts

`diagnose_service`

Walks the agent through health, deployment, resources and dependencies, and tells it to change nothing.

`deploy_and_verify`

Deploys, waits, checks health, and asks you before rolling anything back.

FAQ

## Frequently Asked Questions

Common questions about this service.

### Which clients can connect?

Any MCP client that speaks Streamable HTTP and lets you set an Authorization header — Claude Code, Cursor and VS Code are covered by the snippets above. Clients that only launch stdio servers can reach the same endpoint through the mcp-remote bridge.

### Where do I get the API key?

In the dashboard under Settings → API keys. Give the key the smallest scope that covers what you want the agent to do: read for inspection only, write to let it deploy and restart, admin only if it needs to request secrets or deletions. Keys can be revoked at any time, which cuts the agent off immediately.

### Can the agent read my secrets?

No. list\_env returns variable names with masked values, get\_database never returns a connection URL, and set\_env and set\_secret only carry key names — you type the values in the dashboard. Nothing in the tool surface returns a credential.

### What happens if the agent gets stuck in a loop?

The server allows 60 tool calls per minute per API key and refuses the rest with a message telling the agent to wait. Your plan quotas still apply on top of that, so a runaway agent cannot deploy its way past them.

### Do I have to install or run anything?

No. The server is hosted at https://mcp.runsite.app/mcp and holds no state between calls — it validates your key against the Runsite API on each request and acts as the owner of that key. There is nothing to update when tools are added.

### Does the agent see tools it is not allowed to use?

No. The tool list itself is filtered by the scope of your key, so a read key advertises 13 tools and no deploy tool at all. A call to something outside the scope is refused with an explanation of which scope it would need.

---

> See what's new, improved, and fixed in each release of Runsite.

# Changelog

All notable changes, new features, and bug fixes for Runsite.

September 11, 2026v2.1.0

## CLI and hosted MCP server

Deploy services, read logs and manage environment variables from a terminal or CI with the Runsite CLI. The hosted MCP server exposes 22 tools so AI coding agents can inspect and deploy services, with secrets never returned to the agent and destructive actions gated behind owner approval.

new

September 11, 2026v2.0.0

## Prepaid balance and the Nano tier

Billing moved to a prepaid balance. Every new account starts with €5 of credit, valid 30 days, with no card required. The perpetual free tier for web services and databases was replaced by Nano plans at €3/mo; static sites remain free with no time limit and the 5 GB object-storage allowance is unchanged.

improved

July 18, 2026v1.9.0

## Machine-readable surface for AI agents

Every route now has a Markdown twin served under content negotiation, alongside llms.txt, an RFC 9264 linkset at /.well-known/api-catalog and RFC 8288 Link headers pointing at both. Agents can read the site without parsing HTML.

new

June 15, 2026v1.8.0

## Transactional email

Send receipts, password resets and alerts from EU infrastructure, with SPF, DKIM and DMARC handled. 3,000 messages a month are included, then €1 per additional 1,000.

new

May 23, 2026v1.7.0

## Pricing calculator

Model a full stack — web services, PostgreSQL, Redis and storage — and see the monthly and hourly figures before you create anything.

new

February 28, 2026v1.6.0

## Enhanced Database Management

Improved PostgreSQL management with enhanced monitoring dashboards, automated backup scheduling, and one-click vertical scaling.

new

February 15, 2026v1.5.0

## Real-Time Log Streaming

Stream build and runtime logs in real time via the dashboard. Powered by Redis Pub/Sub for instant feedback during deploys.

newimproved

February 1, 2026v1.4.2

## Billing & Usage Tracking

Pro-rated hourly billing with detailed usage records. View cost breakdowns per service from the new billing dashboard.

new

January 20, 2026v1.4.0

## Horizontal Auto-Scaling

Services on Pro and Business plans now auto-scale based on CPU and memory thresholds with zero-downtime rolling deploys.

newimproved

January 10, 2026v1.3.1

## Faster Build Pipeline

Optimized Docker layer caching reduces average build times by 40%. Incremental builds for Node.js and Python projects.

improved

December 28, 2025v1.3.0

## Custom Domain SSL Fix

Resolved an issue where SSL certificate renewal could fail for domains with complex CNAME chains. Improved retry logic and monitoring.

fixed

---

Source: https://runsite.app/changelog

---

> Runsite is a European deployment platform for web apps, databases, and background jobs. Built for indie developers and small teams who want to ship code, not manage servers.

# About Runsite

Runsite is a deployment platform built for indie developers, freelancers, and small teams. We run the infrastructure so you can focus on writing code and shipping products.

The platform offers web services, managed PostgreSQL and Redis databases, S3-compatible object storage, and scheduled cron jobs — everything a modern web application needs, in one place, at predictable prices.

Runsite is in Public Beta

Every service listed here is live, and paid plans bill exactly as shown. Beta means the platform is young. Features still change, and parts of the roadmap are not built yet.

There is no separate SLA during the beta. The uptime commitments that apply are in [section 8 of the Terms of Service](https://runsite.app/terms#sla). We will publish an SLA with service credits before the beta ends.

## What we stand for

### Built for indie developers

Runsite is designed for solo developers and small teams who want to focus on building products, not managing servers. No YAML configs, no AWS console, no DevOps expertise required.

### European infrastructure

All servers are located in the European Union. Your data never leaves EU borders. GDPR compliance is built in on every plan, and a Data Processing Agreement (DPA) is available at no extra cost upon request.

### Transparent pricing

No per-seat fees, no egress charges, no hidden costs. You pay for the resources your app actually uses, and spending limits ensure you never get a surprise bill.

### €5 of credit, no card

Every new account starts with €5 of credit valid for 30 days. A Nano web service is €3/mo, so on its own it runs the whole month with credit to spare; add a Nano PostgreSQL and the pair runs about 25 days. No credit card, no trial countdown on the app itself: active services stay warm with zero cold starts and only sleep after 14 days without any traffic. Static sites are never billed at all. When the credit is spent, services stop with their data intact rather than quietly billing you.

## Who Runsite is for

Runsite is a European hosting platform, and that is a deliberate choice rather than a marketing line. Every server, database, backup, and object sits in Germany. That makes the platform a good answer for some teams and the wrong answer for others, so here is the honest version of both.

### A good fit if

* You are based in the EU or EEA — or in the UK or Switzerland — and want your infrastructure under European jurisdiction.
* Your users are mostly in Europe, so a German region sits close to them.
* You handle EU personal data and need GDPR residency with a signed DPA, without a sales call or an upcharge.
* You want euro pricing, hourly billing, and a hard spending limit instead of an invoice you find out about later.
* You are a solo developer, freelancer, or small team shipping a product rather than running a platform team.

### Probably not for you if

* You need low latency for users in North America, Asia, Africa, or Latin America. Germany is our only region, and round-trip times from other continents show it.
* You need multi-region failover, global edge deployment, or data replicated across continents.
* You need billing in a currency other than the euro, or data stored outside the EU.
* You are hunting for the cheapest hosting on the market. We optimise for predictable bills and EU residency, not for the lowest sticker price.

You do not have to be an EU company to sign up. Customers elsewhere are welcome where we can lawfully serve them, and their data stays in the EU either way. We do not serve countries or territories under comprehensive EU, UN, UK, or US sanctions, or anyone designated on an applicable sanctions list — the full terms are in [Section 5 of our Terms of Service](https://runsite.app/terms#sanctions).

## Company

Runsite is an independent cloud deployment platform. We are committed to transparent, fair pricing — with no hidden fees, no per-seat charges, and no surprise bills.

For any questions, reach us at [our contact page](https://runsite.app/contact).

## Who writes here

Teo Marquardt

Platform Engineer, Runsite

Platform engineer at Runsite. Builds and operates the deployment platform, the managed PostgreSQL and Redis fleet, and the EU infrastructure they run on — and answers the support mail about all three. Writes here about the failure modes that show up in production rather than in the docs.

Teo is a pen name. Reach the person behind it at [support@runsite.app](mailto:support@runsite.app).

## Operator

Runsite is an independent service operated by an individual developer. There is no corporate entity behind the platform at this stage — it is a solo project built and maintained by one person.

For any inquiries — billing, legal, or technical — contact us at [support@runsite.app](mailto:support@runsite.app).

---

Source: https://runsite.app/about

---

> Get in touch with Runsite support. We typically respond within 24 hours.

# Get in Touch

Have a question, found a bug, or need help? Reach out — we typically respond within 24 hours.

## Email Support

For billing, account issues, and general questions.

[support@runsite.app](mailto:support@runsite.app)

## GitHub

Bug reports, feature requests, and open-source contributions.

[github.com/runsite-platform](https://github.com/runsite-platform)

## Billing & Refunds

For billing questions or refund requests, email [support@runsite.app](mailto:support@runsite.app) with your account email. The unused part of your first top-up is [refundable within 14 days](https://runsite.app/refund).

---

Source: https://runsite.app/contact

---

> Every subprocessor Runsite uses, named, with its legal entity, jurisdiction and transfer basis. Plus where your data is processed and how to get a signed GDPR Data Processing Agreement.

# Compliance

Last updated September 13, 2026

Plenty of platforms describe their subprocessors by category and leave you to guess which companies are behind the words. This page names them and marks the two that sit outside the EEA. If you are running a vendor review and something you need is missing, ask and we will add it.

## Where your data is processed

Application containers, environment variables, build artifacts, managed PostgreSQL and its WAL archive, Redis instances, object storage, backups and logs all run on Hetzner infrastructure in Germany. Nothing replicates to a non-EU region and nothing falls back to overseas infrastructure. Account email leaves through Sweego in France.

Public assets from static sites and public buckets are cached on Cloudflare's edge network so a visitor in Sydney is served from nearby. That cache holds public files. Your database contents, private objects and logs never reach it.

## Subprocessors

Every third party that touches customer data, with the legal entity you would actually be relying on:

| Subprocessor | What it does                                                             | Legal entity                                                 | Data location                                          | Transfer basis                               |
| ------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------ | -------------------------------------------- |
| Hetzner      | Compute, managed PostgreSQL and Redis, object storage, backups, logs     | Hetzner Online GmbH — Gunzenhausen, Germany                  | Germany                                                | Inside the EEA, no transfer mechanism needed |
| Sweego       | Transactional and account email delivery                                 | Mindbaz SAS — Lille, France                                  | France                                                 | Inside the EEA, no transfer mechanism needed |
| Cloudflare   | DNS, TLS termination, and edge caching of public assets and static sites | Cloudflare, Inc. — San Francisco, United States              | Global edge network                                    | Standard Contractual Clauses                 |
| Creem        | Payments, invoicing and VAT, acting as Merchant of Record                | Armitage Labs OÜ (registry code 16977866) — Tallinn, Estonia | EEA, with onward transfers including the United States | Standard Contractual Clauses                 |

Two of the four are worth reading twice. Cloudflare is a US company, which means US law reaches it whatever the edge node's postcode says; it holds public assets only. Creem is Estonian, but its own DPA lists onward transfers to the United States, so the payment and invoicing layer is not EEA-only in the way the infrastructure is. Both are covered by Standard Contractual Clauses.

## Data Processing Agreement

Under GDPR you are the controller for personal data your application handles, and Runsite is the processor. Article 28 requires that relationship to be written down. Write to [support@runsite.app](mailto:support@runsite.app) and you get a signed DPA. Every plan includes it, the free one included. Here is how each obligation is actually met:

| Article  | Obligation                                    | How it is met                                                                                                                                            |
| -------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 28(3)(a) | Process only on documented instructions       | We process customer content only to run the services you have enabled. We do not read it, mine it, or train anything on it.                              |
| 28(3)(b) | Confidentiality of personnel                  | One person operates the platform, under the confidentiality terms of the DPA itself.                                                                     |
| 28(3)(c) | Article 32 security measures                  | Encryption in transit with TLS and at rest with AES-256, isolated per-tenant containers and databases, and access to production limited to the operator. |
| 28(3)(d) | Conditions for engaging subprocessors         | The list on this page is the complete set. Each is bound by its own DPA, and we give notice here before adding one.                                      |
| 28(3)(e) | Assistance with data subject rights           | Export and deletion run from the dashboard. Anything the dashboard cannot do, write to us and we will do it.                                             |
| 28(3)(f) | Assistance with Articles 32 to 36             | We notify you without undue delay of a breach affecting your data, with what we know at the time rather than after an investigation completes.           |
| 28(3)(g) | Deletion or return at the end of the contract | Delete a service and its data and backups are removed within 30 days. Ask for an export first and you get one.                                           |
| 28(3)(h) | Information and audits                        | This page, the DPA, and answers to whatever your security review asks.                                                                                   |

## Who operates Runsite

Runsite is not a registered company. It is operated by an individual developer resident in the EU, which is worth stating plainly because it is the one column where Runsite answers differently from Scalingo SAS or Clever Cloud SAS. If your procurement process needs a company registration number in an EU member state, one of those is the better answer and we would rather you find that out now.

What it does mean is that no US parent company sits above the platform, so there is no entity in the structure that the CLOUD Act can reach. There is also no SOC 2 report and no ISO 27001 certificate. Those cost more than a one-person platform in beta can justify, and claiming an equivalent would be worse than saying so.

## Questions a security review usually asks

Where is my data processed?

Compute, databases, cache, object storage, backups and logs run on Hetzner infrastructure in Germany. Email is delivered by Sweego in France. Both are inside the EEA, so no transfer mechanism applies to them.

Does Runsite use any non-EU subprocessors?

Two. Cloudflare, a US company, handles DNS, TLS and edge caching of public assets. Creem, an Estonian company, handles payments as Merchant of Record and its own subprocessor list includes onward transfers to the United States. Both are covered by Standard Contractual Clauses. Neither holds your database contents, your object storage, or your application logs.

How do I get a signed Data Processing Agreement?

Write to support@runsite.app and we send a signed DPA. It is included on every plan, the free one included, and there is no upgrade, negotiation or sales call in front of it.

What legal entity am I contracting with?

Runsite is not a registered company. It is operated by an individual developer resident in the EU. If your procurement process requires a company registration number in an EU member state, that is a real limitation and you should weigh it before signing.

## Changes to this list

Adding a subprocessor changes who can touch your data, so this page is updated before the change takes effect rather than after, and the date at the top moves with it. If you want to be told directly instead of watching a page, say so at [support@runsite.app](mailto:support@runsite.app).

---

Source: https://runsite.app/compliance

---

> How Runsite collects, uses, and protects your personal data in compliance with GDPR and EU privacy law.

# Privacy Policy

Last updated: September 14, 2026

Applies to: runsite.app and dashboard.runsite.app

Contents

1. [1\. Introduction and Data Controller](#introduction)
2. [2\. Personal Data We Collect](#data-collected)
3. [3\. Legal Basis for Processing](#legal-basis)
4. [4\. How We Use Your Data](#how-we-use)
5. [5\. Data Sharing and Sub-Processors](#data-sharing)
6. [6\. International Data Transfers](#data-transfers)
7. [7\. Data Retention](#data-retention)
8. [8\. Security Measures](#security)
9. [9\. Cookies and Tracking Technologies](#cookies)
10. [10\. Your Rights Under GDPR](#your-rights)
11. [11\. Children's Privacy](#children)
12. [12\. Changes to This Policy](#changes)
13. [13\. Contact and Supervisory Authority](#contact)

## 1\. Introduction and Data Controller

Runsite ("we", "us", or "our") operates a cloud Platform-as-a-Service (PaaS) available at runsite.app. We are committed to protecting the personal data of our users and customers in accordance with the General Data Protection Regulation (EU) 2016/679 ("GDPR") and other applicable EU and EEA privacy laws.

For the purposes of GDPR, Runsite acts as the Data Controller for personal data collected through this website and the platform. Our infrastructure and the data we process on your behalf are hosted in Germany, inside the European Economic Area (EEA). Where we engage third-party service providers who process personal data, we do so under binding Data Processing Agreements (DPAs) that ensure an equivalent level of protection.

For any privacy-related inquiries, please contact us at support@runsite.app.

## 2\. Personal Data We Collect

We collect personal data in the following categories:

* Account data: name, email address, hashed password, and account preferences you provide when registering.
* Billing data: billing name, country, and VAT number where applicable. Full payment card details are handled exclusively by our payment processor and are never stored on Runsite systems.
* Usage data: IP addresses, browser type and version, operating system, pages and features accessed, timestamps, and actions taken within the platform.
* Application data: source code, environment variables, logs, and database contents you deploy to the platform. We process this data solely on your instructions as a Data Processor.
* Support communications: messages you send to our support team, including any personal data contained therein.
* Compliance data: the country of establishment or residence you declare, the origin of your payment instrument, connection geolocation, and the results of screening this information against applicable sanctions lists. We process this to meet our obligations under sanctions and export control law, as described in Section 5 of our Terms of Service.
* Cookie and tracking data: session identifiers and analytics identifiers described in Section 8.

## 3\. Legal Basis for Processing

We process your personal data only where we have a valid legal basis under Article 6 GDPR:

| Legal Basis                              | Processing Purpose                                                                                                                                                                    |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Performance of a contract (Art. 6(1)(b)) | Creating and managing your account, provisioning and operating platform resources, processing payments, and providing customer support.                                               |
| Legitimate interests (Art. 6(1)(f))      | Ensuring platform security, detecting and preventing abuse and fraud, improving our services through aggregated analytics, and enforcing our Terms of Service.                        |
| Legal obligation (Art. 6(1)(c))          | Compliance with EU tax and accounting requirements, screening accounts and payments against applicable sanctions lists, and responding to lawful requests from competent authorities. |
| Consent (Art. 6(1)(a))                   | Sending promotional communications and placing non-essential cookies. You may withdraw consent at any time without affecting the lawfulness of prior processing.                      |

## 4\. How We Use Your Data

* Provision and operation of the platform, including deployment pipelines, managed databases, and scheduled jobs.
* Processing payments and issuing invoices, including VAT where required under EU regulations.
* Sending transactional notifications (deployment status, billing receipts, security alerts). These are part of the service and cannot be opted out of while you hold an active account.
* Providing technical support and responding to inquiries.
* Detecting, investigating, and preventing security incidents, abuse, and violations of our Terms of Service.
* Conducting aggregated, anonymised analysis to improve platform performance and features.
* Fulfilling legal and regulatory obligations.

We never sell your personal data to third parties, and we never use your application data for advertising or model training purposes.

## 5\. Data Sharing and Sub-Processors

We use the sub-processors below to handle personal data, and each one is bound by a DPA with obligations consistent with GDPR. For the legal entity, data location and transfer basis of each platform sub-processor, see our [Compliance page](https://runsite.app/compliance).

* Hetzner Online GmbH (Germany): compute, managed PostgreSQL and Redis, object storage, backups and logs.
* Sweego, operated by Mindbaz SAS (France): transactional and account email.
* Cloudflare, Inc. (United States): DNS, TLS termination, edge caching of public assets and static sites, and cookieless page-view analytics for runsite.app.
* Creem, operated by Armitage Labs OÜ (Estonia): payments, invoicing and VAT as Merchant of Record. Creem is legally responsible for the sale and tax remittance, and its own sub-processors include transfers to the United States.
* Formspree (United States): the email address you submit through the early access form on runsite.app.

Before we add a sub-processor, we list it on the [Compliance page](https://runsite.app/compliance).

We do not disclose personal data to any other third party except when required to do so by applicable law, court order, or to protect the rights, property, or safety of Runsite, our users, or the public. We will notify you of any such disclosure to the extent permitted by law.

## 6\. International Data Transfers

The platform runs in Germany. Data leaves the EEA in three places: Cloudflare, the onward sub-processors Creem uses, and Formspree. Those transfers rely on the safeguards required by Chapter V of GDPR, such as:

* An adequacy decision by the European Commission (e.g., EU–US Data Privacy Framework).
* Standard Contractual Clauses (SCCs) adopted by the European Commission.
* Binding Corporate Rules (BCRs) approved by a competent supervisory authority.

You may request a copy of the applicable transfer mechanisms by contacting support@runsite.app.

## 7\. Data Retention

We retain personal data only for as long as necessary to fulfil the purpose for which it was collected, or as required by law:

* Account data is retained for the duration of your account and deleted within 30 days of account closure, unless a longer retention period is required by law.
* Billing and invoice records are retained for 7 years to comply with EU accounting and tax regulations.
* Platform logs (access logs, deployment logs) are retained for up to 90 days for security and debugging purposes.
* Support communications are retained for 2 years from the date of resolution.
* Sanctions screening records, including a positive or negative match result and the data it was based on, are retained for 5 years from the date of the check, in line with standard compliance record-keeping practice.
* When a PostgreSQL database is stopped because your balance or starting credit has run out and its storage is later reclaimed, its final backup is kept for 12 months from that date, so the data can be restored after a top-up. You may request earlier deletion at any time.
* Anonymised and aggregated analytics data may be retained indefinitely as it no longer constitutes personal data.

## 8\. Security Measures

We implement appropriate technical and organisational measures to protect personal data against unauthorised access, accidental loss, destruction, or disclosure, including:

* Encryption of data in transit using TLS 1.2 or higher.
* Encryption of data at rest using AES-256.
* Tenant-level isolation: each customer's resources and data are logically separated.
* Role-based access controls limiting internal access to personal data on a need-to-know basis.
* Regular vulnerability scanning and security reviews.
* Incident response procedures with notification timelines compliant with Art. 33 GDPR (72-hour supervisory authority notification) and Art. 34 GDPR (data subject notification where required).

No method of transmission over the internet or electronic storage is 100% secure. We encourage you to use a strong, unique password and enable two-factor authentication on your account.

## 9\. Cookies and Tracking Technologies

We use the following categories of cookies:

* Strictly necessary cookies: required for authentication, session management, and CSRF protection. These cannot be disabled without impairing core functionality.
* Analytics cookies: used to measure aggregate usage patterns (page views, feature adoption). These are set only with your consent and process data in an anonymised or pseudonymised form where possible.

You can manage or withdraw cookie consent at any time via your browser settings or our cookie preference centre. Withdrawing consent for analytics cookies does not affect the operation of the platform.

## 10\. Your Rights Under GDPR

As a data subject in the EU/EEA, you have the following rights under GDPR:

* Right of access (Art. 15): request a copy of the personal data we hold about you.
* Right to rectification (Art. 16): request correction of inaccurate or incomplete data.
* Right to erasure (Art. 17): request deletion of your personal data where no overriding legal basis exists for retention.
* Right to restriction of processing (Art. 18): request that we limit how we use your data in certain circumstances.
* Right to data portability (Art. 20): receive your personal data in a structured, commonly used, machine-readable format and transmit it to another controller.
* Right to object (Art. 21): object to processing based on legitimate interests, including profiling.
* Right to withdraw consent (Art. 7(3)): withdraw consent at any time where processing is consent-based, without affecting the lawfulness of prior processing.
* Right to lodge a complaint: you have the right to lodge a complaint with your national supervisory authority or the lead supervisory authority in the EU.

To exercise any of these rights, contact us at support@runsite.app. We will respond within 30 days as required by Art. 12 GDPR. Identity verification may be required before we fulfil your request.

## 11\. Children's Privacy

Our platform is not directed at individuals under the age of 16\. We do not knowingly collect personal data from children. If you believe we have inadvertently collected data from a minor, please contact us immediately at support@runsite.app and we will delete it promptly.

## 12\. Changes to This Policy

We may update this Privacy Policy from time to time to reflect changes in our practices or applicable law. When we make material changes, we will notify you by email (at the address associated with your account) and by updating the "Last updated" date at the top of this page. We encourage you to review this policy periodically. Continued use of the platform after the effective date of any changes constitutes your acceptance of the revised policy.

## 13\. Contact and Supervisory Authority

For questions, requests, or complaints regarding this Privacy Policy or our data practices, please contact:

* Email: support@runsite.app

If you are not satisfied with our response, you have the right to lodge a complaint with the supervisory authority in your EU/EEA member state of residence or the competent lead supervisory authority.

---

Source: https://runsite.app/privacy

---

> Terms and conditions governing your use of the Runsite platform, compliant with EU consumer and commercial law.

# Terms of Service

Last updated: September 11, 2026

Applies to: runsite.app and dashboard.runsite.app

Contents

1. [1\. Agreement and Acceptance](#agreement)
2. [2\. Description of Services](#services)
3. [3\. Account Registration and Security](#account)
4. [4\. Acceptable Use Policy](#acceptable-use)
5. [5\. Service Availability, Sanctions and Export Control](#sanctions)
6. [6\. Customer Content and Intellectual Property](#content-ip)
7. [7\. Billing, Payments, and Refunds](#billing)
8. [8\. Service Level Commitment](#sla)
9. [9\. Data Processing](#data-processing)
10. [10\. Confidentiality](#confidentiality)
11. [11\. Warranties and Disclaimers](#warranties)
12. [12\. Limitation of Liability](#liability)
13. [13\. Indemnification](#indemnification)
14. [14\. Term and Termination](#termination)
15. [15\. Force Majeure](#force-majeure)
16. [16\. Governing Law and Dispute Resolution](#governing-law)
17. [17\. General Provisions](#general)

## 1\. Agreement and Acceptance

These Terms of Service ("Terms") constitute a legally binding agreement between you ("Customer", "you", or "your") and Runsite, a service operated by an individual developer ("we", "us", or "our"), governing your access to and use of the Runsite cloud platform available at runsite.app and dashboard.runsite.app (the "Platform"). Operator information and contact details are available on our About page at runsite.app/about.

By creating an account, clicking 'I agree', or otherwise accessing the Platform, you confirm that you have read, understood, and agree to be bound by these Terms and our Privacy Policy. If you are accepting these Terms on behalf of a company or other legal entity, you represent that you have the authority to bind that entity.

If you do not agree to these Terms, you must not create an account or use the Platform. We reserve the right to update these Terms at any time. Material changes will be communicated by email and by updating the 'Last updated' date above. Continued use of the Platform after the effective date of any change constitutes acceptance of the revised Terms.

## 2\. Description of Services

Runsite is a Platform-as-a-Service (PaaS) providing managed cloud infrastructure, including but not limited to:

* Web application hosting and deployment pipelines (Web Services, Static Sites).
* Managed relational and in-memory databases (PostgreSQL, Redis).
* Scheduled job execution (Cron Jobs).
* Object and file storage (S3-compatible storage).
* SSL certificate provisioning and custom domain management.
* Platform API and CLI tooling.

We reserve the right to modify, expand, or discontinue any feature or service component at any time. Where we discontinue a paid service you actively use, we will provide at least 30 days written notice, except where immediate action is required for security or legal compliance reasons.

Static Sites hosting is the only service provided without charge. All other services, including Web Services, PostgreSQL, Redis, Cron Jobs, object storage and email, are paid services billed as described in Section 7 and on our Pricing page at runsite.app/pricing. New accounts receive a one-time starting credit that can be spent on paid services, as set out in Section 7.

Static Sites hosting, the starting credit, promotional offers, and beta services are provided as-is, without any service level commitment, and may be modified, limited, suspended, or discontinued at any time without prior notice.

## 3\. Account Registration and Security

To use the Platform, you must be at least 18 years old, or the age of majority in your jurisdiction, and have the legal capacity to enter into a binding contract. You must register an account and provide accurate, complete, and current information. You agree to:

* Maintain the confidentiality of your login credentials and not share them with any third party.
* Be solely responsible for all activity that occurs under your account, whether or not authorised by you.
* Notify us immediately at support@runsite.app if you become aware of any unauthorised access to or use of your account.
* Ensure that all users accessing the Platform under your account comply with these Terms.
* Keep your account information (including billing details and contact email) accurate and up to date.

We recommend enabling two-factor authentication (2FA) on your account. We will never ask for your password via email or support channels.

## 4\. Acceptable Use Policy

You may use the Platform only for lawful purposes and in accordance with these Terms. The following activities are strictly prohibited:

* Hosting, transmitting, or distributing content that is illegal, defamatory, fraudulent, or infringes upon any third-party intellectual property or privacy rights.
* Operating, promoting, or facilitating online gambling, casinos, betting, lotteries, or similar games of chance.
* Hosting or distributing pornographic, sexually explicit, or adult content, and any content involving the sexual exploitation or abuse of minors.
* Hosting, distributing, or promoting content that depicts, incites, or glorifies violence, terrorism, or self-harm.
* Advertising, selling, or facilitating the trade of weapons, firearms, ammunition, explosives, or other regulated or controlled goods.
* Advertising, selling, or facilitating the trade of illegal drugs, controlled substances, or other prohibited products.
* Sending unsolicited communications (spam) or operating services designed to facilitate spam.
* Distributing malware, ransomware, spyware, or any malicious software.
* Conducting, facilitating, or enabling distributed denial-of-service (DDoS) attacks or any form of network abuse.
* Mining or generating cryptocurrency without explicit prior written consent from Runsite.
* Operating VPN endpoints, proxy servers, VLESS, Xray, V2Ray, Shadowsocks or similar relay nodes, or any service whose purpose is to tunnel or forward third-party network traffic. This applies on every plan, paid plans included, and detected services are blocked without notice.
* Creating multiple accounts, or using automated, fake, or fraudulent identities, to circumvent usage limits, evade suspension or bans, or otherwise abuse the Platform.
* Abusing the starting credit, free Static Sites hosting, or promotional resources, including exploiting them for commercial-scale workloads, reselling them, or using them beyond their intended fair-use purpose.
* Circumventing or attempting to circumvent any security, access control, or rate-limiting mechanism of the Platform.
* Using the Platform in a manner that adversely affects the performance, availability, or stability of shared infrastructure for other customers.
* Impersonating any person or entity, or falsely representing your affiliation with any person or entity.
* Processing, storing, or transmitting payment card data in a manner inconsistent with PCI-DSS requirements.
* Any activity that violates applicable EU or national law, including but not limited to GDPR, the EU Digital Services Act, and anti-money-laundering regulations.
* Any use of the Platform that breaches applicable sanctions, trade restrictions, or export control law, or that is intended to help any person circumvent them. See Section 5.

Violation of this policy may result in immediate account suspension, termination, and permanent deletion without refund, at our sole discretion. Accounts found in serious or repeated violation — including those hosting prohibited content listed above — may be permanently blacklisted, preventing the registration of future accounts. We will cooperate with law enforcement authorities in investigations of illegal activity.

Moderation and enforcement. We monitor compliance with this policy through a combination of manual review and, where deployed, automated detection tools. Anyone may report suspected prohibited or infringing content hosted on the Platform by contacting support@runsite.app; we review such reports in accordance with applicable law, including the EU Digital Services Act. If we determine that specific content or services violate these Terms, we may remove or disable access to that content, or suspend the associated account, in accordance with Section 14\. Where required by law, we will notify you of the action taken and the reasons for it. If you believe an enforcement action was taken in error, you may appeal by contacting support@runsite.app; we will review appeals in good faith and respond within a reasonable time.

## 5\. Service Availability, Sanctions and Export Control

Runsite is a European platform. All infrastructure — compute, managed databases, object storage, and backups — is operated within the European Union, and the Platform is built and marketed first and foremost for customers established in the EU and the wider European Economic Area (EEA). Customers established elsewhere may be accepted at our discretion, provided that supplying them is lawful under the restrictions set out in this Section and that they accept EU data residency and these Terms.

We comply with all economic sanctions, trade restrictions, and export control laws applicable to us, including restrictive measures adopted by the European Union and its Member States, measures imposed by the United Nations Security Council, and — to the extent they apply to us, to our payment processor acting as Merchant of Record, or to our infrastructure suppliers — the sanctions programmes of the United Kingdom and of the United States (administered by OFAC). We apply non-EU sanctions programmes only to the extent that doing so is consistent with Council Regulation (EC) No 2271/96 (the EU Blocking Statute) and other applicable EU law, which take precedence.

By registering an account and for as long as you use the Platform, you represent, warrant, and undertake that:

* You are not located in, ordinarily resident in, established in, incorporated under the laws of, or acting on behalf of a person in, a country or territory subject to comprehensive sanctions or embargo. As at the 'Last updated' date of these Terms, this includes Russia, Belarus, Iran, North Korea (DPRK), Cuba, Myanmar (Burma), and the territories of Ukraine that are not under the control of the Government of Ukraine, namely Crimea, Sevastopol, and the non-government-controlled areas of the Donetsk, Luhansk, Zaporizhzhia, and Kherson oblasts. This list is indicative and not exhaustive: sanctions regimes are adopted, amended, and lifted over time, and the restriction always applies to the measures in force at the relevant time rather than to the list reproduced here.
* You are not a person, entity, vessel, or body designated on any applicable sanctions list, including the EU Consolidated Financial Sanctions List, the UN Security Council Consolidated List, the UK Sanctions List, or the OFAC Specially Designated Nationals (SDN) list; nor are you owned as to 50% or more, or otherwise controlled, directly or indirectly, by any such designated person.
* You will not use the Platform to supply, resell, sublicense, host, or otherwise make available infrastructure, services, software, technology, or data to or for the benefit of any restricted country, territory, or designated person, and you will not use the Platform to circumvent, or assist any third party in circumventing, any sanction or export control restriction.
* Your use of the Platform will not breach applicable export control law, including Regulation (EU) 2021/821 on dual-use items, and you will not deploy on the Platform any technology or software whose export, transfer, or provision would require an authorisation you do not hold.
* Payments will not be made from a bank, card, or payment instrument issued in a restricted jurisdiction, or by or on behalf of a designated person.
* You will notify us at support@runsite.app without undue delay if any of these representations ceases to be accurate.

Screening and verification. We may screen accounts, account holders, beneficial owners, and payment details against applicable sanctions lists at registration, at the time of payment, and periodically thereafter, and may use the geolocation of connections and payment origin as part of that assessment. We may request documentation evidencing your identity, place of establishment, or ownership structure, and may decline to provision or continue services until it is provided. Any personal data processed for this purpose is handled in accordance with our Privacy Policy and on the legal basis of compliance with a legal obligation.

Enforcement. If we determine, or have reasonable grounds to suspect, that you are in breach of this Section, or that continuing to serve you would place us in breach of applicable sanctions or export control law, we may — without prior notice and without liability to you — suspend or restrict access to your account and services, decline or reverse payments, withhold or freeze funds where the law requires it, terminate the account under Section 14, and delete Customer Content. Where sanctions law obliges us to report a suspected breach, or prohibits us from notifying you of an action taken or from issuing a refund, we will comply with that law in priority to any notice or refund obligation elsewhere in these Terms. Where we are permitted to do so, we will notify you of the action and its reason, and you may contest our assessment by contacting support@runsite.app with supporting evidence; we review such submissions in good faith and will reinstate services if the restriction proves to have been applied in error.

Amounts paid in respect of a suspended or terminated account are not refundable to the extent that a refund is itself restricted by applicable sanctions law, or where the suspension follows a breach of this Section. This does not affect statutory rights that cannot be waived under applicable EU consumer law.

## 6\. Customer Content and Intellectual Property

You retain all rights, title, and interest in and to any code, data, files, databases, configurations, or other content you deploy to or store on the Platform ("Customer Content"). We do not claim any ownership rights in Customer Content.

You grant Runsite a limited, non-exclusive, royalty-free licence to host, store, process, and transmit Customer Content solely as necessary to provide the Platform services to you. This licence terminates when you delete the relevant content or close your account.

You represent and warrant that: (a) you own or have the necessary rights to Customer Content; (b) Customer Content does not infringe the intellectual property or other rights of any third party; and (c) Customer Content complies with all applicable laws.

You are solely responsible for maintaining independent backups of Customer Content outside the Platform. While we may provide backup features for certain services, we do not guarantee the availability, completeness, or recoverability of any backup, and to the maximum extent permitted by applicable law we are not liable for any loss or corruption of Customer Content.

All rights in the Platform itself — including its software, APIs, user interface, design, documentation, and branding — remain the exclusive property of Runsite and are protected by applicable intellectual property laws. You may not copy, modify, create derivative works from, reverse-engineer, or extract any part of the Platform.

## 7\. Billing, Payments, and Refunds

Paid services are funded from your account balance. Top-ups of that balance are processed by our third-party payment processor, who acts as Merchant of Record (MoR) for all transactions. As MoR, the payment processor is the legal seller of record, is responsible for collecting and remitting applicable taxes (including VAT), and issues all invoices and receipts directly to you. Runsite is the service provider. By topping up your balance, you authorise the charge of the selected amount to your chosen payment method.

Starting credit for new accounts:

* Every new account receives a one-time starting credit of €5, valid for 30 days from registration. No payment method is required to receive it.
* Charges are drawn from the starting credit first and from your paid balance only once the credit has been used up. The starting credit is not a payment: it has no cash value, cannot be withdrawn, transferred, or refunded, and does not count toward lifetime spend, so it does not unlock higher plan tiers or production email sending.
* While an account runs on the starting credit alone, it may hold one service of each type (one web service, one PostgreSQL database, one Redis instance, and one cron job) on entry plans only. Static Sites are not counted. A first top-up of at least €5 lifts these limits.
* When the starting credit is used up, or 30 days have passed since registration, whichever comes first, services running on the credit are stopped and any unused remainder expires. Your paid balance is not affected.

Prices and charges:

* Current plans and prices are published on our Pricing page at runsite.app/pricing. Plan prices are quoted per month and paid services are billed by the hour, pro rata, while they are running. Charges are deducted from your balance.
* Usage beyond a plan's included allowances, such as database storage above the included quota, object storage, or email above the included monthly quota, is charged at the rates shown on the Pricing page.
* Static Sites are not billed and never draw on your balance or starting credit. We will notify you by email at least 30 days before introducing any charge for Static Sites, including pay-as-you-go bandwidth above the included quota.
* Web services on the entry Nano plan that receive no requests for 14 consecutive days may be suspended and are resumed by the next incoming request. Starter plans and above are not suspended for inactivity.
* You may set a monthly spending limit in the dashboard. When usage reaches that limit, services are paused automatically and no further charges accrue until you raise the limit or the next cycle begins.
* All prices are displayed inclusive or exclusive of VAT as required by applicable law. VAT and other applicable taxes are calculated and collected by our Merchant of Record at the point of purchase and will be itemised on your invoice.
* Because our payment processor acts as MoR, their applicable terms of service may govern the payment transaction. You may receive invoices and payment communications from our payment processor rather than from Runsite directly.
* We reserve the right to change pricing with a minimum of 30 days written notice. Price changes take effect no earlier than 30 days after that notice.

When your balance runs out:

* If your paid balance and any remaining starting credit are exhausted, running paid services are stopped. Nothing is charged to a payment method without your authorisation. We notify you by email before this happens where we can anticipate it, and again when services are stopped.
* Stopped services and their data are retained for 7 days. Topping up within that period and starting the service restores it with its data intact.
* After 7 days, PostgreSQL storage volumes are reclaimed and the final backup of each PostgreSQL database is kept for 12 months, so it can be restored after a later top-up. Redis instances are not backed up, and their data cannot be recovered once their storage is reclaimed.
* You remain solely responsible for exporting or independently backing up any data you wish to keep, as set out in Section 6\. We may change these retention periods on 30 days notice to the email address registered on your account.

Charges already deducted from your balance are non-refundable, and remaining paid balance is refundable only: (a) for the unused part of your first top-up, if requested within 14 days of that top-up, as described in our Refund Policy at runsite.app/refund; (b) where required by applicable EU consumer law; (c) as described in our Service Level Commitment in Section 8; or (d) in cases of billing error, which you must dispute within 60 days of the charge by contacting support@runsite.app. Refund processing is handled by our Merchant of Record; we will initiate the process on your behalf upon receipt of a valid request.

Right of withdrawal for consumers: if you are a natural person acting for purposes outside your trade, business, or profession ('consumer') and you top up your balance or purchase a paid plan, you have the right to withdraw from the contract within 14 days of the date of purchase, in accordance with the EU Consumer Rights Directive (2011/83/EU), unless you have expressly requested and we have commenced performance of the service within that period. The starting credit is not a purchase and gives rise to no refund.

## 8\. Service Level Commitment

We target high availability for all platform services. While the Platform is in Public Beta there is no separate Service Level Agreement and no contractual uptime guarantee: the commitments in this section are the ones that apply. We will publish an SLA with per-tier uptime targets and a credit schedule before Public Beta ends, and will give notice before it takes effect.

* Scheduled maintenance windows communicated with at least 48 hours advance notice are excluded from uptime calculations.
* Downtime caused by Customer Content, customer-side configuration errors, or factors outside our reasonable control (including force majeure events) is excluded.
* Where we grant a credit for an outage, it is applied to future invoices, has no cash value, and is your sole and exclusive remedy for that outage.
* Requests relating to an incident must reach us within 30 days of it.

If an outage has affected you, write to support@runsite.app and we will look at what happened and what we can do about it.

## 9\. Data Processing

To the extent that you store or process personal data (as defined under GDPR) of third parties on the Platform, you act as the Data Controller and Runsite acts as the Data Processor. In this capacity:

* Runsite will process such personal data only on your documented instructions and as necessary to provide the Platform services.
* Runsite will implement appropriate technical and organisational measures to protect personal data against unauthorised access, loss, or destruction.
* Runsite will assist you in responding to data subject requests and in fulfilling your obligations under applicable data protection law.
* Runsite will notify you without undue delay if it becomes aware of a personal data breach affecting your data.
* Runsite will delete or return all personal data upon termination of the agreement, at your choice, unless retention is required by law.

A full Data Processing Agreement (DPA) is available upon request at support@runsite.app. Customers subject to GDPR may be required to execute a DPA as a condition of using certain platform features. Our sub-processor list is maintained and available upon request.

## 10\. Confidentiality

Each party may receive information from the other party that is designated as confidential or that reasonably should be understood to be confidential given the nature of the information and circumstances of disclosure ("Confidential Information"). Confidential Information does not include information that: (a) is or becomes publicly known through no fault of the receiving party; (b) was rightfully known before receipt; (c) is independently developed without reference to the Confidential Information; or (d) is required to be disclosed by law or court order.

Each party agrees to: (a) use the other's Confidential Information solely to exercise its rights and fulfil its obligations under these Terms; (b) protect it with the same degree of care used for its own confidential information, but no less than reasonable care; and (c) not disclose it to third parties without the disclosing party's prior written consent, except to employees or contractors who need to know it and are bound by equivalent confidentiality obligations.

## 11\. Warranties and Disclaimers

We warrant that: (a) we have the legal right and authority to enter into and perform under these Terms; (b) the Platform will perform materially in accordance with our documentation; and (c) we will not knowingly introduce malicious code into the Platform.

Except as expressly stated above, the Platform is provided "as is" and "as available". To the maximum extent permitted by applicable law, we disclaim all warranties, express or implied, including warranties of merchantability, fitness for a particular purpose, and non-infringement. We do not warrant that the Platform will be uninterrupted, error-free, or that all defects will be corrected.

Nothing in these Terms excludes or limits any statutory rights that cannot be waived under applicable EU consumer protection law.

## 12\. Limitation of Liability

To the maximum extent permitted by applicable EU law, neither party shall be liable to the other for any indirect, incidental, special, consequential, exemplary, or punitive damages, including loss of profits, loss of revenue, loss of data, loss of goodwill, or business interruption, even if advised of the possibility of such damages.

Subject to the paragraph below, our total aggregate liability for all claims arising under or in connection with these Terms shall not exceed the greater of: (a) the total fees paid by you to Runsite in the 12 months immediately preceding the event giving rise to the claim; or (b) EUR 100.

Nothing in these Terms limits or excludes either party's liability for: (a) death or personal injury caused by negligence; (b) fraud or fraudulent misrepresentation; (c) any liability that cannot be excluded or limited by applicable EU or national law; or (d) wilful misconduct or gross negligence.

## 13\. Indemnification

You agree to indemnify, defend, and hold harmless Runsite and its officers, directors, employees, and agents from and against any claims, damages, losses, liabilities, costs, and expenses (including reasonable legal fees) arising out of or relating to:

* Your use of the Platform in violation of these Terms.
* Your Customer Content, including any claim that it infringes the rights of a third party.
* Your violation of any applicable law or regulation.
* Any misrepresentation made by you in connection with these Terms.

## 14\. Term and Termination

These Terms remain in effect for as long as you have an active account with us.

* You may terminate your account at any time by deleting it from the dashboard or by contacting support@runsite.app. Hourly charges for a service stop when that service is deleted.
* We may suspend your account immediately if we have reasonable grounds to believe you are violating these Terms, including the Acceptable Use Policy, or if required for security reasons. We will notify you of the suspension and the reason.
* We may terminate your account with 30 days written notice for any reason, or immediately in cases of material or repeated breach of these Terms, illegal activity, or non-payment.
* Upon termination, your access to the Platform will cease. We will delete your Customer Content and personal data within 30 days of account closure, unless a longer retention period is required by applicable law.
* Provisions of these Terms that by their nature should survive termination — including Sections 5, 6, 9, 10, 11, 12, 13, and 16 — shall survive termination.

## 15\. Force Majeure

Neither party shall be liable for any failure or delay in performance under these Terms to the extent such failure or delay is caused by circumstances beyond its reasonable control, including but not limited to: acts of God, natural disasters, pandemic, war, terrorism, civil unrest, government action, embargoes, internet infrastructure failures, or actions of third-party service providers outside our reasonable control.

The party affected by a force majeure event will: (a) promptly notify the other party; (b) use commercially reasonable efforts to mitigate the impact; and (c) resume performance as soon as reasonably practicable. If a force majeure event continues for more than 30 consecutive days, either party may terminate the affected services by written notice.

## 16\. Governing Law and Dispute Resolution

These Terms and any disputes arising out of or in connection with them shall be governed by and construed in accordance with the laws of England and Wales, without regard to conflict of law principles.

Before initiating formal proceedings, each party agrees to attempt to resolve any dispute through good-faith negotiations for a period of 30 days after written notice of the dispute is provided.

If informal resolution fails, disputes shall be submitted to the exclusive jurisdiction of the courts of England and Wales. If you are a consumer resident in the EU/EEA, you may also be entitled to use the European Commission's Online Dispute Resolution (ODR) platform, accessible at ec.europa.eu/consumers/odr.

If you are a consumer, nothing in this section affects your rights to bring proceedings in the courts of your country of residence under applicable EU consumer law.

## 17\. General Provisions

* Entire agreement: these Terms, together with the Privacy Policy and any applicable DPA or SLA, constitute the entire agreement between the parties regarding the Platform and supersede all prior agreements.
* Severability: if any provision of these Terms is found to be unenforceable, that provision shall be modified to the minimum extent necessary to make it enforceable, and the remaining provisions shall continue in full force and effect.
* Waiver: failure by either party to enforce any provision of these Terms shall not constitute a waiver of the right to enforce that or any other provision in the future.
* Assignment: you may not assign or transfer your rights or obligations under these Terms without our prior written consent. We may assign these Terms in connection with a merger, acquisition, or sale of all or substantially all of our assets, with notice to you.
* Notices: we may send notices to the email address associated with your account. Legal notices to Runsite must be sent to support@runsite.app and are effective upon confirmed receipt.
* Language: these Terms are written in English. In case of conflict between translated versions and the English version, the English version shall prevail.

---

Source: https://runsite.app/terms

---

> Runsite refund policy for a prepaid balance with hourly billing. The unused part of your first top-up is refundable within 14 days.

# Refund Policy

Last updated: September 11, 2026

Contents

1. [1\. How Billing Works](#how-billing-works)
2. [2\. 14-Day Refund of Unused Balance](#guarantee)
3. [3\. EU Right of Withdrawal](#eu-withdrawal)
4. [4\. How to Request a Refund](#how-to-request)
5. [5\. Stopping Services and Closing Your Account](#stopping-services)
6. [6\. Charges Already Incurred](#usage-charges)
7. [7\. Payment Disputes and Chargebacks](#disputes)
8. [8\. Complaints](#complaints)
9. [9\. Exceptions](#exceptions)

## 1\. How Billing Works

Runsite does not sell subscriptions. You top up a prepaid account balance, and paid services draw on that balance by the hour, pro rata, while they are running. Current plans and prices are published on our Pricing page at runsite.app/pricing, and the full billing terms are in Section 7 of our Terms of Service.

* Top-ups are processed by our payment processor, who acts as Merchant of Record and issues your invoices and receipts.
* Every new account receives a one-time starting credit of €5, valid for 30 days. The starting credit is not a payment, is spent before your paid balance, and is never refundable.
* Static Sites are not billed and never draw on your balance or starting credit.

This policy therefore covers money you have paid into your balance: when the unused part of it can be refunded, and which charges cannot be.

## 2\. 14-Day Refund of Unused Balance

If you are not satisfied with Runsite for any reason within 14 days of your first top-up, contact us and we will refund the part of that top-up that is still unused on your paid balance — no questions asked.

* The refundable amount is the unused part of your first top-up at the time we receive your request. Hourly charges already deducted for the time your services ran, and other charges already incurred, are not refunded.
* Because the starting credit is spent before your paid balance, the credit itself is never part of a refund.
* The guarantee applies once per customer and only to the first top-up on an account. Later top-ups are not covered, except under the EU right of withdrawal in Section 3.
* Once the refund is approved, the refunded amount is removed from your balance. Paid services that the remaining balance cannot cover are stopped, as described in Section 7 of our Terms of Service.

## 3\. EU Right of Withdrawal

If you are a consumer resident in the European Union or European Economic Area, you have a statutory right to withdraw from a distance contract within 14 calendar days of a top-up, in accordance with the EU Consumer Rights Directive (2011/83/EU), without giving any reason.

Please note: if you expressly request that we begin providing services before the withdrawal period ends, for example by starting a deployment or running a paid service, you pay for the services provided up to the moment you withdraw. The refund is the amount of that top-up less the charges for those services, which is the same unused amount described in Section 2.

To exercise your right of withdrawal, contact us at support@runsite.app before the 14-day period expires. We will send you an acknowledgement and process any applicable refund promptly.

## 4\. How to Request a Refund

To request a refund under the 14-day refund of unused balance or the EU right of withdrawal, email us at support@runsite.app with the following information:

* The email address associated with your Runsite account.
* The date and amount of the top-up you want refunded.
* A brief reason for the refund (optional, but helpful for us to improve).

We will confirm receipt of your request within 2 business days. Because our payment processor acts as Merchant of Record, approved refunds are processed by our payment processor and returned to the original payment method used for the top-up. Processing typically completes within 5–10 business days; the exact timeline may vary depending on your bank or card issuer.

## 5\. Stopping Services and Closing Your Account

There is no subscription to cancel. Hourly charges for a service stop when you delete that service in the dashboard, and you can set a monthly spending limit that pauses services automatically when it is reached.

Deleting services or closing your account does not by itself trigger a refund. Unused paid balance is refundable within the 14-day window in Section 2 or under the EU right of withdrawal in Section 3\. Outside those cases, remaining balance is not refunded, except where applicable law requires otherwise.

## 6\. Charges Already Incurred

Refunds do not apply to charges already deducted from your balance, including but not limited to:

* Hourly charges for the time your paid services were running.
* Storage used in excess of a plan's included quota, and object storage.
* Email sent above the included monthly quota.
* Bandwidth or egress charges where applicable.

These charges reflect infrastructure resources that have already been consumed and cannot be recovered. If you believe a charge is incorrect, contact us at support@runsite.app within 60 days of the charge and we will investigate.

## 7\. Payment Disputes and Chargebacks

If you have a billing issue or believe you have been charged incorrectly, please contact us at support@runsite.app before initiating a chargeback with your bank or card issuer. Because payments are handled by our Merchant of Record, we will work with them to investigate and resolve the issue. Most billing issues can be resolved quickly and directly.

Initiating a chargeback without first contacting us may result in temporary account suspension while the dispute is under review. We reserve the right to recover legitimate charges through the dispute resolution process provided by our payment processor (Merchant of Record).

## 8\. Complaints

If you are not satisfied with the outcome of a refund request or have any other complaint regarding our billing or service, you may submit a formal complaint to support@runsite.app. Please include 'Formal Complaint' in the subject line along with a clear description of the issue.

* We will acknowledge your complaint within 2 business days.
* We aim to provide a full resolution within 14 business days. If additional time is needed, we will notify you with an estimated timeline.
* If your complaint is not resolved to your satisfaction, you may escalate it to the relevant consumer authority in your EU/EEA country of residence.
* EU residents may also use the European Commission's Online Dispute Resolution (ODR) platform at ec.europa.eu/consumers/odr to seek an out-of-court resolution.

## 9\. Exceptions

We reserve the right to decline a refund request in the following circumstances:

* The request is submitted more than 14 days after the first top-up and does not qualify as a statutory EU right of withdrawal.
* The account has been suspended or terminated due to a violation of our Terms of Service or Acceptable Use Policy.
* Repeated use of the 14-day refund across multiple accounts.
* The request relates solely to charges already incurred, or to the starting credit.

In all cases we will notify you by email with the reason for our decision. If you are an EU consumer and believe our decision is incorrect, you may contact your national consumer protection authority or use the European Commission's Online Dispute Resolution platform at ec.europa.eu/consumers/odr.

---

Source: https://runsite.app/refund
