Skip to main content

PocketBase vs. a Cluster API Fleet: The Three Walls That Tell You When to Switch

9 min readDora NodaDora Noda
Share

PocketBase is a single ~30MB Go binary that boots a full backend — SQLite database, REST and realtime API, file storage, auth, and an admin dashboard — with no Docker, no docker-compose.yml, and no config file to write before your first request. On a $4/month Hetzner CAX11, its own FAQ claims it holds 10,000+ concurrent realtime connections. A Cluster API-managed Kubernetes fleet, by contrast, needs a minimum of three control-plane nodes just to survive one machine failure, plus etcd, a CNI, cert rotation, and a provider like CAPH before it schedules a single tenant workload. These are not two competing ways to run the same app — they're opposite ends of the self-hosting complexity spectrum, and most teams pick wrong at least once.

The Decision, Up Front

Before the walkthrough, here's the actual threshold table — the concrete numbers that decide which side of this you're on:

SignalStay on PocketBaseMove to a Cluster API fleet
Concurrent DB writersBursty, low-hundreds at peakSustained high-concurrency writes from many independent services
Realtime connectionsUnder ~10K on a $4–20/mo boxNeeds horizontal scale-out across machines
Failure toleranceA few minutes of downtime for a restart is fineNeed automatic failover with zero operator involvement
Tenancy modelOne app, one team, or a handful of trusted usersMultiple tenants needing hard resource/security isolation
Ops budgetOne person, no dedicated infra timeA team that can own etcd, CNI, and node lifecycle

If every row on the left describes your project, standing up Cluster API for it is over-engineering, not maturity. If two or more rows on the right apply, PocketBase isn't a smaller version of what you need — it's structurally the wrong tool. The rest of this post is why, with the actual numbers behind each row.

What's Actually in the Single Binary

"No Docker, no dependencies" is a real claim, not marketing shorthand. PocketBase embeds SQLite directly via Go's modernc.org/sqlite driver (a pure-Go SQLite implementation with no CGO requirement, which is what lets it cross-compile into one static binary at all), and since version 0.23 it splits state into two SQLite files: data.db for your actual collections and records, and auxiliary.db for logs and ephemeral system metadata — both running in WAL mode with their own -wal/-shm journal files. On top of that sits the REST API, an SSE-based realtime subscription system, built-in auth (password, OAuth2, OTP), file storage (local disk or S3), and an embedded admin UI, all compiled into one artifact you scp to a box and run.

That's the entire operational surface: one process, one directory of SQLite files, no separate database server to patch, no message broker, no sidecar. For a huge swath of real projects — a solo founder's SaaS, an internal tool, a mobile app backend, a prototype that needs to exist by Friday — that's not a compromise. It's strictly less to operate than the equivalent stack built from a Postgres container, a Redis container, and a hand-rolled auth service, and PocketBase's own FAQ is upfront that this is the whole design bet: it scales vertically, on a single server, by design.

Where PocketBase Is the Right Call — With Numbers

PocketBase's official FAQ claims 10,000+ persistent realtime connections on a $4/month Hetzner CAX11 (2 vCPU, 4GB RAM), and community benchmarking discussions on higher-end dedicated hardware put sustained throughput at 10,000+ requests/second and 100,000+ concurrent realtime connections. That's not a toy number — it's more traffic than most side projects, internal tools, and early-stage SaaS products will ever see in production.

Set that against the floor cost of a Cluster API-managed alternative: three Hetzner nodes just for a stacked-etcd control plane (the accepted minimum for HA, since etcd quorum of 2-out-of-3 is what lets the cluster survive one node going down) run roughly €4.35–€8/month each post-2026 pricing — call it €15–24/month before you've scheduled a single worker pod, added a load balancer, or paid anyone's time to keep etcd, the CNI, and node images patched. For a workload PocketBase's single $4 box handles comfortably, that's not "more robust" — it's paying a recurring infrastructure tax, plus real day-2 operational surface (cert rotation, node upgrades, provider CRDs), for headroom the workload will never use.

The decision framework here is genuinely simple: if your traffic and write pattern fit inside one process on one machine, a fleet is a solution to a problem you don't have yet.

The Three Walls Where It Runs Out of Room

PocketBase doesn't degrade gracefully as you scale past its comfort zone — it hits hard architectural walls, each traceable to a specific design decision. These are the three concrete points where "just get a bigger box" stops working.

Wall 1: Concurrent Writers Past SQLite's Comfort Zone

SQLite's WAL mode allows unlimited concurrent readers, but exactly one writer at a time, database-wide — that's not a tunable limit, it's the file format. Under synchronous=NORMAL, a single writer can push somewhere in the neighborhood of 70,000–100,000 small write-transactions per second in isolation, which sounds like plenty. But that number assumes nothing else is contending for the lock; real-world reports of SQLITE_BUSY/"database is locked" errors start showing up once roughly 100+ concurrent writer threads are competing for that single write slot, and PocketBase's own internals reflect the constraint directly — its connection pool allows around 120 concurrent readers but caps writes at a single connection.

That's fine for a request pattern where writes are occasional relative to reads (the common web-app shape). It stops being fine the moment you have many independent processes — background workers, webhook consumers, multiple app instances — all trying to write to the same PocketBase instance at meaningful concurrency. There's no read-replica-plus-write-primary escape hatch here, because there's only one writer, period, and it's the same process serving your HTTP requests.

Wall 2: Multi-Node HA It Was Never Built For

PocketBase's FAQ states this outright: it scales "only on a single server, aka. vertical." There is no built-in clustering, no leader election, no automatic failover — the process either is running, or it isn't. GitHub Discussions on the project ("Pocketbase and its scaling," "Scaling issue?", "Need help with scaling pocketbase in production") converge on the same answer every time someone asks about running multiple instances: community forks that add replication tend to break the realtime subscription system and JS hooks, because both are implemented at the application layer against a single in-process SQLite handle, not against a distributed backend. Litestream — the officially recommended companion tool — replicates the WAL to object storage for backup and disaster recovery, but that's point-in-time restore after a failure, not zero-downtime failover during one.

If "a few minutes of downtime while a systemd unit restarts the process" is an acceptable failure mode for your app, this isn't a real wall. If a tenant SLA requires automatic failover with no human in the loop, PocketBase structurally cannot deliver it — there's no config flag that adds a second writer.

Wall 3: Per-Tenant Isolation It Doesn't Have

PocketBase runs as one OS process with one set of resource limits, one auth realm, and one set of collections. There's no native concept of hard-isolated tenants — no per-tenant CPU/memory quota, no per-tenant network policy, no blast-radius containment if one tenant's workload misbehaves or gets compromised. You can build soft multi-tenancy into your data model (a tenant_id column and API rules that filter by it), but that's isolation enforced by your application code, not by the platform underneath it — a bug in a collection rule is a cross-tenant data leak, not a contained failure.

A Cluster API-managed fleet gets you real isolation primitives instead: per-namespace resource quotas, network policies scoping which pods can talk to which, and — if you need it harder than a namespace boundary — a dedicated node pool or even a dedicated cluster per tenant. That's the actual thing you're buying when you cross this wall: not "more scale" in the abstract, but isolation guarantees enforced below the application layer.

What You Actually Buy Once You Cross the Line

None of this makes Cluster API "better" — it makes it a different tool solving a different problem. A CAPI-managed fleet (CAPH on Hetzner, for instance) gives you: a control plane that survives a node dying because etcd quorum tolerates it, not because a script restarted a process; horizontal scale-out where adding capacity means joining a new machine to the fleet rather than upgrading the one box you have; and namespace- or cluster-level tenant isolation enforced by Kubernetes RBAC and network policy rather than by application code you have to get right every time.

The cost of that is real and worth naming honestly: a minimum of three nodes instead of one, etcd as a stateful system you now operate, a CNI to choose and patch, and meaningfully more day-2 surface than "SSH in and restart the binary." That tradeoff only pays for itself once you've actually hit one of the three walls above — sustained multi-writer contention, a real HA requirement, or hard tenant isolation. Standing up the fleet before you need it is the same mistake as staying on PocketBase after you've outgrown it, just in the other direction.

If you do cross that line, Bex.co is the open-source, AI-native Render alternative built on exactly this fleet model — push a git repo and get a running HTTPS service on Cluster API-managed machines you own, without hand-rolling the CAPH bootstrap yourself. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex