---
title: "PostgreSQL Connection Pooling for Mobile Backends: PgBouncer vs pgpool-II vs Supavisor"
published: true
description: "Compare PgBouncer, pgpool-II, and Supavisor for mobile backends. Survive push-notification traffic spikes and connection exhaustion without over-engineering your stack."
tags: postgresql, mobile, api, architecture
canonical_url: https://mvpfactory.co/blog/postgresql-connection-pooling-mobile-backends
---
Mobile traffic breaks naive Postgres connection handling. In this post I will show you how to pick the right connection pooler and size it correctly so push-notification floods and app-launch waves do not take your backend down.
What you will learn:
- Why mobile traffic patterns are uniquely hostile to raw Postgres connections
- When to use PgBouncer, pgpool-II, or Supavisor
- The pool sizing formula that prevents connection exhaustion without over-provisioning
Prerequisites: A running Postgres instance, basic familiarity with connection strings, and a mobile backend that is already feeling — or anticipating — connection pressure.
The mobile traffic problem
Let me show you a pattern that shows up on every mobile backend at scale. Three burst shapes keep appearing:
- Cold app launches — Users opening the app after overnight sleep hit your backend in a synchronized wave. Everyone woke up, got a push from another service, and now they are checking yours.
- Push-notification spikes — A single FCM/APNs broadcast generates tens of thousands of simultaneous API calls within 30 seconds. I have watched this take down well-provisioned servers.
- Foreground/background cycling — iOS and Android aggressively kill and restore network state, creating connection churn that long-lived session pools cannot absorb.
A max_connections = 200 Postgres instance with no pooler falls over under a modest push to 50k devices. Each idle Postgres backend consumes roughly 5–10 MB of RAM and holds a worker process. At 200 connections you have already allocated 1–2 GB just for connection overhead before a single query runs.
The contenders
PgBouncer — reach for this first
Lightweight C daemon, single-threaded event loop. Ships in every Linux package manager and runs on under 2 MB of RAM.
Three modes exist, but only one matters for mobile. Session mode gives each client one server connection for the duration — equivalent to no pooling, skip it. Statement mode breaks multi-statement transactions — skip it. Transaction mode releases the server connection after each transaction completes. This is what you want.
Here is the minimal setup to get this working:
; pgbouncer.ini — transaction mode config
[pgbouncer]
pool_mode = transaction
max_client_conn = 5000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
pgpool-II — probably not what you need
Feature-rich middleware: connection pooling, query load balancing across replicas, in-memory query cache, and replication management. For mobile backends that need only connection pooling, pgpool-II adds unnecessary complexity — a heavier process model, more failure modes, and query routing logic that misbehaves with ORMs. Reserve it for architectures that genuinely need transparent read/write splitting.
Supavisor — for multi-tenant SaaS
Elixir-based pooler built by Supabase. Each tenant gets an isolated pool, preventing one noisy mobile client from starving another. Runs as a cluster-aware service rather than a per-node daemon. The tradeoff is real: operationally heavier than PgBouncer (requires an Erlang/OTP runtime), but the isolation model is genuinely better when your backend serves multiple independent organizations.
| Dimension | PgBouncer | pgpool-II | Supavisor |
|---|---|---|---|
| RAM footprint | ~2 MB | ~50–100 MB | ~50–200 MB |
| Transaction mode | Yes | Yes | Yes |
| Multi-tenant isolation | No | No | Yes |
| Operational complexity | Low | High | Medium |
| Best fit | Single-tenant APIs | HA + replica routing | Multi-tenant SaaS |
Pool sizing math
Here is the formula I use in every project. Most teams set default_pool_size by feel — that is how you end up either starving Postgres or leaving capacity on the table. Size from the database outward, not the client inward:
pool_size = (postgres_max_connections × 0.8) / number_of_pgbouncer_instances
The 0.8 factor reserves headroom for superuser connections and monitoring. For a max_connections = 200 Postgres with two PgBouncer nodes:
pool_size = (200 × 0.8) / 2 = 80 per node
Set max_client_conn to your 99th-percentile concurrent connection estimate, not your average. A reserve_pool_size of 10–15% of default_pool_size absorbs the initial spike while the pool warms.
Gotchas
Transaction mode and prepared statements. Here is the gotcha that will save you hours. PgBouncer historically broke named prepared statements because the underlying server connection changes between transactions. PgBouncer 1.21 fixed this with protocol-level prepared statement tracking — no ini key required, just upgrade. If you are stuck on an older release, configure your driver to use unnamed prepared statements (prepareThreshold=0 in pgjdbc, prepared_statement_cache_queries=0 in asyncpg). See the PgBouncer 1.21 changelog for specifics.
Multi-database pool budgets. PgBouncer maintains pools per (database, user) pair, not as a global shared allocation. If your backend connects to multiple databases through the same PgBouncer instance, budget pool_size independently per database — dividing by instance count alone will underestimate total server connections.
Supavisor read replica routing. The docs do not always surface this, but Supavisor's read replica routing works at the connection level based on explicit client hints, not automatic query-level read/write detection. Verify against your ORM's behavior before relying on it.
Conclusion
Default to PgBouncer 1.21+ in transaction mode for any mobile backend not serving multiple tenants. Calculate default_pool_size from max_connections, apply the 0.8 headroom factor, then account for per-database pool budgets before dividing across instances. Only reach for Supavisor when tenant isolation is a hard requirement — the operational cost is not justified for single-product mobile backends. pgpool-II is overkill unless you need read replicas baked into the middleware layer.
Pick the simplest pooler that solves your actual problem.
Top comments (0)