Why Neon serverless PostgreSQL hits connection limits
The serverless model of Neon is appealing because the database adapts to load on its own and requires no manual administration.
The bottleneck shows up not in average mode but at peaks: when requests from users, background processing, and reports arrive simultaneously, the application hits the database connection limit even though compute resources are still available.
The trap lies in the connection pool itself. Each application instance holds its own sessions, and with horizontal scaling their number grows linearly: ten instances with ten connections each is already a hundred connections.
As soon as the total pool exceeds the allowed limit, new requests do not open a session but fail with a connection error — while the service is fully operational.
The second scenario — database pause during idle time. At night or in the off-season, traffic drops, the compute node goes to sleep, and the first request after the pause waits for wake-up instead of receiving a response.
The user gets an error or a noticeable delay, and support gets a false alarm “database unavailable.”
Both scenarios trigger during the same hours — at peak, when revenue depends on a second of response time. A shortage of connections in the application triggers retries, which increase load, and some requests time out.
We explain how this is resolved by configuring the pool, the wake-up mode, and role separation — without abandoning the serverless approach.
Neon branches: isolated databases for development, testing, and checks
A database branch in Neon is a clone based on copy-on-write: branching transfers not the rows themselves but references to pages, so the child database appears in seconds regardless of size.
After that, changes are written only to it, and deleting the branch frees only those pages that were actually modified in it.
This model provides isolated development environments for each task, check, or experiment.
Migrations and heavy queries are run on a copy of production data without affecting the main database, and the state at a desired point in time is reproduced by branching again from any point.
Environment preparation time drops from hours of deploying a dump to seconds: the developer and tester get a copy with the current schema without waiting for a shared environment to become available. Parallel checks run simultaneously and do not block each other.
Warming up the database before load testing stops being a separate procedure: the branch is brought up in advance, typical queries are run so that execution plans and the page cache reach a working state, and only then are measurements taken. Without warm-up, the first requests use cold plans and distort the picture.
Service downtime during releases is reduced because migrations have been tested on a realistic data volume rather than an empty schema. If something goes wrong, rollback is switching to the previous branch, not restoring from a backup.
Comparison of connection options: direct, through a pool, and through a proxy
Serverless functions and containers open a connection on every invocation, and PostgreSQL serves each connection with a separate process. At peak, short-lived clients exhaust the connection limit, and the application receives a refusal instead of a response.
Choosing a connection mode is a trade-off between the number of simultaneous clients and compatibility with familiar code.
In practice, there are three options: direct connection to the database, a connection pool, and an intermediate connection layer. They are not mutually exclusive — for migrations and background tasks, it is reasonable to keep a separate connection string.
| Option | When it fits | Limitations |
|---|---|---|
| Direct connection to the database | Migrations, background tasks, long analytical queries, debugging | One client — one process on the server; the connection limit is exhausted quickly |
| Connection pool, transaction mode | Web requests and serverless function calls: many short operations | The connection is pinned only for the duration of the transaction — prepared statements, advisory locks, LISTEN/NOTIFY, and session temporary tables are unavailable |
| Connection pool, session mode | Code that holds session state and prepared statements | The connection is occupied until the client disconnects, so the gain in client count is smaller |
| Intermediate connection layer | Edge compute where every new connection is expensive | One query per invocation, without interactive transactions and cursors |
The choice of connection mode is determined by the load profile: long tasks use a direct connection, application queries use a pool in transaction mode, and short edge invocations use the intermediate layer.
Connection strings differ by parameters, so switching modes does not require rewriting application code.
Workflow: from query audit to handover of procedures
We start with an audit, not with configuration: we collect statistics on query execution plans, look for sequential scans on large tables, and identify connections that remain open noticeably longer than a typical application query.
In parallel, we analyze the data schema — field types, integrity constraints, migration history: some performance drops lie not in queries but in table structure.
-
Query and data schema audit. We collect the top queries by time and frequency, mark those whose time grows linearly with data volume, and record table and index sizes and connection limits.
-
Database schema design. We bring types in line with actual data, add missing indexes and constraints, and separate hot and analytical operations so that heavy queries do not compete with user requests.
-
Environment configuration. We set the compute node size and suspension policy, the connection pool, and read replicas; for development and testing we create separate Neon database branches — experiments do not affect production.
-
Failure testing. We run peak load, node failure, connection drops, and point-in-time recovery; we record recovery time and the queries that degrade first.
-
Migration procedure. We describe applying schema changes without long locks, the rollback order, and integrity checks after each migration.
-
Handover to operations. We provide configurations, schema, monitoring metrics, recovery instructions, and the escalation procedure, and review everything with the customer’s team.
As a result, the team has a reproducible configuration and procedures, not manual actions that live only in one engineer’s head.
What we hand over: schemas, roles, policies, procedures
Database handover is not granting console access but a package by which your team can itself bring up a copy of the environment, apply a migration, and restore data without messaging us.
Below is what exactly we record in the project repository and documentation: from schema description to backup procedure. A Neon branch for migration testing is created from a snapshot — testing runs on a copy of production data, not on an empty database.
- Data schema and migrations — versioned SQL files in application order, separate scenarios for a clean deployment and for applying to an existing database, a log of applied versions in a service table.
- Application connection configuration — connection string parameters for the pool and for service connections, read/write separation, environment variables for each environment, timeout and retry requirements on connection loss.
- Access policies and roles — list of roles: schema owner, application, read-only analytics, on-call; permission boundaries at the table and sequence level, prohibition of shared accounts.
- Backup procedure — snapshot schedule, retention period, restore verification order, and acceptable data loss point for each environment.
- Database documentation — description of entities and relationships, purpose of service tables, list of heavy queries and supporting indexes, known limitations.
- Recovery scenario — step-by-step instructions with integrity checks and rollback to the previous snapshot, tested in a staging environment.
- Observability map — which connection, latency, and error metrics signal what and to whom.
We accept each item together with your engineer: they go through the list, bring up the environment from our files themselves, and restore the database. If something cannot be reproduced from the documentation, the item is not closed.
Case study: separated environments and eliminated failures under peak load
Before separation, production, test, and preview environments lived in one database: shared schema, shared compute limit, one connection pool. Migrations from a test task changed tables that clients were already using, and during the evening peak some requests timed out.
Over a quarter, there were about 40 incidents, and recovery after an unsuccessful release took 40–60 minutes: deploy a dump, compare the schema, catch up data.
What we changed
We configured Neon so that each environment received a separate database branch: production, test, and a branch for each development task.
The branch is created by copying state at the storage level — without exporting and importing a dump, so an environment for testing comes up in minutes.
Compute scales with load and stops on inactive branches, and the connection pool is maintained on the service side and is not consumed by short connections.
Access was separated by roles: only the on-call shift can see the production branch, and migrations go through a separate environment with a branch backup before the run.
What we achieved
After two months of operation: peak timeout failures — zero instead of 5–6 per week, the share of successful requests grew from 97.2% to 99.98%.
Recovery time after a failure decreased by about 20 times — from 40–60 minutes to 2–3: returning to the previous branch state instead of deploying a dump. Preparing an environment for a task — 5 minutes instead of two days of waiting and manual assembly.
The test environment now mirrors production in data and schema, so discrepancies between test and production no longer cause surprises. The schema withstood a seasonal peak three times higher than normal traffic without expanding the infrastructure.
How a managed database differs from a manually deployed instance
The difference is not in PostgreSQL itself — the engine is the same. The difference is in who owns the operational layer: a managed database takes on high availability, backups, updates, and scaling under load, while a self-managed instance leaves that to the team.
A managed environment wins where there is more than one environment: database branching provides a separate copy of data for each task, and inactive environments go to sleep on their own.
For example, Neon in serverless mode takes the assembly of test environments off the engineer’s plate — they are created from a branch in minutes.
| Option | Who is responsible for infrastructure | Labor costs |
|---|---|---|
| Managed service | provider: backups, high availability, updates | hours per month |
| Instance on a rented server | team: patches, replication, backups, monitoring | on-call duty and maintenance windows |
| Self-hosted cluster | team and infrastructure department | a dedicated role on staff |
Self-managed database maintenance is not a one-time setup but a continuous cycle: security updates, restore verification from backups, monitoring table growth and disk space, and investigating query degradation.
Individually, each item is small; together they are a regular workload that does not end after launch.
The contractor’s area of responsibility is the same in any option: data schema, indexes, slow queries, zero-downtime migrations, and access rights. Only who is responsible for the layer below — the service and hardware — changes.
The choice here is not about quality but about where the team is ready to maintain ongoing responsibility.
Concerns during migration: compatibility, migration, rollback
Concerns during migration are rarely related to the serverless approach itself — it is more important not to lose working data and to have a predictable way back.
Before switching, we define three things: how the database is migrated, what happens with extensions, and what rights the application retains.
How to migrate a working database with minimal downtime?
The main path is logical replication: the target database is included in the change stream and catches up with the source, and traffic is switched only after that. We monitor lag down to seconds, then compare row counts in key tables and checksums. Writes are stopped for minutes — for the final catch-up and changing connection strings.
Are the required extensions supported?
The set of available extensions in a managed service is narrower than on a self-hosted server. Before migration, we compare installed extensions against the supported list: whatever is not supported — we move into the application or leave on the managed cluster. We separately check full-text search and types that extensions replace with their own.
Will the application retain the right to change the schema?
We separate rights: the application runs under a role with data access, and the schema is changed by a separate migration role.
Migrations are applied with versioning and run on a database copy before production — the service does not modify tables at startup and does not break compatibility with the previous code version.
What if we have to roll back?
The rollback plan is fixed before work begins: the source database remains operational, and reverse replication lives until the new platform has gone through a full load cycle.
The point of no return is disabling the reverse stream: before it, rollback takes as long as the forward switch; after it, recovery is from a backup.
Let’s discuss the task: we’ll put together a schema and come back with an estimate and plan
To come back with a timeline estimate and a database work plan, we do not need full access to the entire project — the inputs that determine the storage architecture are enough.
Below is a short list: if some item is not yet formulated, we will discuss the task and fill in what is missing together.
- Data schema and volume: main tables, relationships between them, estimated number of rows in the largest ones, and their growth rate.
- Load profile: peak and average read/write request rates, number of simultaneous connections, response time requirements for critical screens.
- Access scenarios: where connections come from — server application, background tasks, reporting, administrative scripts; whether separate isolation of roles and rights is needed.
- Schema versioning: where migrations live, how they are applied, whether there is a proven procedure for returning to the previous state.
- Data durability requirements: recovery depth to an arbitrary point in time, frequency of backup verification, acceptable unavailability window during restore.
- Placement and access: where users and services are located, whether separate database copies are needed for development, pre-release testing, and incident investigation.
We come back not with a “verbal” estimate: we show the placement diagram, connection pool and idle database suspension configuration, migration plan, and post-switch verification procedure.
For projects on Neon, we separately discuss how database branches and autoscaling fit your load profile — the schema and timelines depend on that.
Describe the task — we will come back with an estimate and work plan.







