Scaling PocketBase: Performance, Limits and Production

Scaling PocketBase: Performance, Limits and Production
“Can PocketBase scale?” is the question I get most. The short answer: for the read-heavy apps most of us build — SaaS dashboards, mobile backends, internal tools, content sites — yes, on one server, further than you’ll probably need. The long answer is knowing where the one real limit is and what to do before you hit it. That’s this post.
PocketBase vs SQLite: what’s the difference?
People search for this a lot, so let’s clear it up first. SQLite is a database engine. PocketBase is a backend built on top of it. SQLite stores and queries your data; PocketBase adds everything around it:
| SQLite | PocketBase | |
|---|---|---|
| What it is | An embedded database library | A backend server in one binary |
| REST API | No | Yes, generated from your collections |
| Auth (email, OAuth2, OTP, MFA) | No | Yes |
| Access control | No | API rules per collection |
| Realtime | No | Yes, over Server-Sent Events |
| File storage | No | Yes — local disk or S3-compatible |
| Admin dashboard | No | Yes |
| Server-side logic | No | JavaScript hooks and Go extensions |
So “PocketBase vs SQLite” isn’t really a choice — if you’d otherwise write the API, auth, and admin layer yourself around a database, PocketBase is that layer, already written.
How PocketBase handles load
The database lives in the same process as the API, so there’s no network hop between your code and your data. Reads are fast and run concurrently. PocketBase runs its database in WAL mode, so readers don’t block the writer.
The one real trade-off: writes are serialized — one write transaction at a time. Each write is short, so for typical apps this queue is never noticed. It starts to matter when you have sustained, high-concurrency writes: event ingestion, analytics firehoses, chat at large scale.
Realtime holds up well too: a single instance on 2 CPU cores and 4 GB of RAM comfortably serves 1,000+ concurrent realtime connections. For reproducible numbers on your own hardware, the PocketBase project publishes its benchmark suite — run it rather than trusting anyone’s blog post, including this one.
Scale up first
PocketBase is designed to scale vertically: when you need more, give it a bigger machine. There’s no cluster to coordinate, no connection pool, no replication lag to reason about. On PocketBase Cloud that step is the Pro plan’s dedicated compute — your own 2 vCPU machine instead of a shared server — at a flat price. See plans and billing.
Before you move to bigger hardware, though, fix the three things that cause most slow PocketBase apps.
1. Add indexes for what you filter and sort on
A slow list query almost always means a missing index. If you frequently run:
pb.collection("posts").getList(1, 10, {
filter: 'category = "tech"',
sort: "-views",
});
then category and views want an index. Add it in the dashboard’s
collection editor (the Indexes section), or in a migration so every
environment gets it — see
shipping migrations to a running instance.
2. Keep expand shallow
Deeply nested expansion (expand: "user.company.settings") turns one
request into a chain of lookups. Denormalize the field you actually display,
or fetch related data in a separate, simpler query.
3. Fetch only the fields you render
pb.collection("posts").getList(1, 20, { fields: "id,title,slug,author" });
Smaller responses mean less serialization on the server and less parsing on the client.
A smaller client-side tip: the JS SDK cancels a pending request when an
identical one is sent with the same requestKey, so a user hammering a
button doesn’t queue five copies of the same query:
await pb.collection("users").getList(1, 1, {
filter: 'email = "test@example.com"',
requestKey: "email_check",
});
PocketBase limitations, honestly
PocketBase is a great fit for most apps, and a poor fit for a few. Know these before you commit:
- It’s pre-1.0. The project’s own docs say backward compatibility isn’t guaranteed before v1.0.0, and upgrades occasionally include breaking changes. Pin your version and read the changelog before upgrading.
- Writes are serialized (above). Sustained heavy concurrent writes belong on a client-server database.
- It scales up, not out. There’s no built-in multi-node replication or read replicas. One instance, one machine.
- No Postgres. No PostGIS, no pgvector, no Postgres extensions — see Can PocketBase use Postgres?
- Hooks aren’t Node.js. JavaScript hooks run on an embedded engine, so npm packages that need Node APIs won’t load. For that, run a separate backend next to your instance.
If none of those is a deal-breaker for your app, PocketBase’s simplicity is a genuine advantage, not a compromise.
Is PocketBase good for production?
Yes — with the same discipline you’d give any production system. My checklist:
- Pin the PocketBase version. On PocketBase Cloud,
--pb-version(or the pin inpbc.json) controls exactly which release runs; upgrade deliberately, after testing locally with the same binary. - Back up continuously, and test a restore. Continuous backup streams every database change off-site on paid plans, and lets you restore to any point in the last 7 days. Uploaded files aren’t in the database — back them up separately or keep them on S3-compatible storage.
- Lock down every collection’s API rules before launch. An empty rule means “anyone” — see collections and API rules and our security best practices.
- Watch the numbers. Request rate, latency, and resource use per instance tell you when you’re approaching the limits above — usage and performance metrics.
- Keep migrations in version control so production’s schema is never a hand-edited surprise.
Conclusion
PocketBase scales by staying simple: one process, one machine, no distributed-systems tax. Index what you query, keep responses small, know the write-concurrency limit, and it’ll carry most products a long way. When you’d rather not run the server yourself, deploy it on PocketBase Cloud.