BuildWithMatija
Get In Touch
Build with Matija logo

Build with Matija

Modern websites, content systems, and AI workflows built for long-term growth.

Services

  • Headless CMS Websites
  • Next.js & Headless CMS Advisory
  • AI Systems & Automation
  • Website & Content Audit

Resources

  • Case Studies
  • How I Work
  • Blog
  • CMS Hub
  • E-commerce Hub
  • Dashboard

Headless CMS

  • Payload CMS Developer
  • CMS Migration
  • Payload vs Sanity
  • Payload vs WordPress
  • Payload vs Contentful

Get in Touch

Ready to modernize your stack? Let's talk about what you're building.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
  1. Home
  2. Blog
  3. Tools
  4. Neon Postgres Architecture: Copy Connection Model First

Neon Postgres Architecture: Copy Connection Model First

Understand Neon's storage/compute separation, PgBouncer transaction pooling, and what to reproduce on self‑hosted…

9th May 2026·Updated on:2nd May 2026·MŽMatija Žiberna·
Tools
Neon Postgres Architecture: Copy Connection Model First

📚 Get Practical Development Guides

Join developers getting comprehensive guides, code examples, optimization tips, and time-saving prompts to accelerate their development workflow.

No spam. Unsubscribe anytime.

What Neon's Architecture Actually Changes, and What You Can Copy in a Self-Hosted Setup

Reviewed on April 21, 2026. By Matija Me — independent developer specializing in Next.js, Payload CMS, and production Postgres deployments.


If you have used Neon and wondered why it feels different from a conventional hosted Postgres instance under heavy connection load, the answer is architectural. Neon separates storage and compute at the platform level, replaces single-node durability with a distributed WAL quorum, and puts PgBouncer in front of everything. Most of that is not cheaply reproducible on a single VPS. One major part of it is — and that part is worth understanding.

This article is for engineers running OLTP-style workloads who want to understand what Neon is actually doing differently and which pieces of that behavior translate to a self-hosted environment. It is not a claim that every workload will outperform a well-tuned local Postgres. The useful takeaway is narrower: separate your connection-management problems from your storage-engine problems, and address the connection side first. That distinction follows directly from Neon's documented architecture and PgBouncer's documented behavior.


The Core Architectural Move

Neon is open source. Its public GitHub repository describes it as a serverless Postgres platform that separates storage and compute. PostgreSQL remains the execution engine, but durable state is no longer owned by the compute node in the traditional single-machine sense.

Neon's architecture docs describe two independent layers connected by WAL. The compute layer is ephemeral — it runs Postgres and uses RAM and local NVMe for performance, but it does not own durable state and can be replaced freely. The storage layer handles correctness, history, and scale. It is built around three components: safekeepers, the pageserver, and object storage.

That separation is the architectural move that matters most. Neon did not just tune Postgres. It changed the boundary between execution and durability.


Why Neon Behaves Differently Under Connection-Heavy Workloads

Neon's docs support three specific reasons it handles many connection-heavy application workloads well: local-first reads, a write path defined by WAL acknowledgment quorum, and PgBouncer-based connection multiplexing.

Reads. The compute node checks RAM first, then local NVMe, and only then asks the pageserver for a page. Neon explicitly states that object storage is kept off the critical query path. The compute node never reads directly from object storage.

Writes. A transaction is considered committed once WAL has been acknowledged by a quorum of safekeepers, via Paxos. The compute node does not wait for data pages to be written to disk or object storage before acknowledging the commit. That write path is faster than it looks on paper because the durability burden moves to the safekeeper layer, not to local page flushing.

Connections. Neon's pooling docs say PgBouncer can accept up to 10,000 client connections. Those are not 10,000 simultaneous queries. Neon is explicit that the client connection ceiling, the per-pool active limit, and real Postgres backend concurrency are different numbers. Understanding that distinction is the most transferable lesson in this entire article.


What to Actually Measure

If you copy Neon's app-facing connection pattern, the headline connection count is the wrong metric. The right metrics are: real Postgres backends before and after adding PgBouncer, queue time at the pooler, average and tail latency under bursty traffic, timeouts, and whether session-sensitive workflows like migrations and dumps are actually bypassing the pooler.

Neon's monitoring docs distinguish between pooler client connections, pooler server connections, and total Postgres connections. That three-way distinction is a useful mental model for any Postgres deployment, not only Neon.

One operational detail worth tracking from the start: PgBouncer's config docs warn that increasing max_client_conn can require higher OS file descriptor limits, and that potential descriptor use can exceed max_client_conn once pool overhead is included. Get ahead of this before you push client connection limits upward.


The Part Most Self-Hosters Should Copy First

The highest-return part of Neon to reproduce is the connection model, not the storage engine.

Neon's pooling docs describe a PgBouncer setup built around transaction pooling, a high client connection ceiling, and a per-user, per-database pool model. As of the January 10, 2025 changelog, Neon uses default_pool_size = 0.9 * max_connections — a formula that replaced an earlier fixed pool size of 64. That change reflects a real operational learning: pool size should be derived from actual backend capacity, not set to an arbitrary constant.

Modern web and serverless traffic is often connection-heavy but backend-light. Many clients connect, but only a smaller number of real server connections need to exist simultaneously when transactions are short and connections are reused efficiently. Neon's monitoring and pooling docs both reflect that distinction between many client connections and a smaller set of actual Postgres server connections. The goal is to absorb connection churn at the pooler layer without forcing Postgres to maintain idle backends for every open client socket.


What Transaction Pooling Buys You, and What It Breaks

Neon runs PgBouncer in transaction mode, which means connections are returned to the pool after each transaction completes. PgBouncer's features documentation shows exactly why this is powerful and where the sharp edges are: transaction pooling intentionally does not support features that depend on persistent session state.

The specific incompatibilities documented in Neon's pooling docs and PgBouncer's features page are worth knowing before you deploy:

FeatureWorks in transaction pooling
Session-level SET / RESETNo
LISTEN / NOTIFYNo
WITH HOLD CURSORNo
SQL-level PREPARE / DEALLOCATENo
Temporary tables with PRESERVE ROWSNo
Session-level advisory locksNo
Protocol-level prepared statementsYes (with caveats)

A concrete example of how this breaks in production: SET search_path can appear to work within one transaction and then silently fail on the next, because the next transaction may land on a different server connection that does not carry the session setting forward. Neon's recommended fixes are to use a direct connection when session settings must persist, fully qualify schema names in your queries, or set defaults at the role level.

That is why any self-hosted design copying this model needs two database paths: a pooled endpoint for normal application traffic, and a direct endpoint for migrations, dump and restore, logical replication, admin tasks, and anything session-sensitive. That split matches Neon's own guidance on when to use pooled versus direct connections.


A Practical Self-Hosted Blueprint

You can reproduce a meaningful part of Neon's application-facing behavior on a single VPS without rebuilding a distributed database platform. The shape of the configuration matters more than any single magic number.

Run PostgreSQL directly on the fastest local storage available. Keep max_connections conservative enough that the database remains stable under real load — the number of active backends should reflect what the hardware can actually serve, not optimistic headroom. Put PgBouncer in front of Postgres and use transaction pooling for normal application traffic. Expose a second direct DSN for migrations, dump and restore, replication, admin tasks, and anything that relies on session state.

Size your active pool from real backend capacity: default_pool_size = 0.9 * max_connections is Neon's current formula and a reasonable starting point. Tune OS file descriptor limits if you push max_client_conn above the default. Use finite wait time at the pooler instead of unbounded queueing — an explicit timeout surfaces backpressure before it becomes an outage.

The operational posture to copy alongside the configuration: short transactions, aggressive connection reuse, explicit retry behavior in application code, health checks, and clean separation between normal app traffic and privileged direct workflows.


What You Cannot Cheaply Copy

Reproducing Neon's safekeeper-based commit model on a single ordinary Postgres instance is not feasible without significant additional infrastructure. Neon defines commit durability through WAL acknowledgment by a quorum of safekeepers, via Paxos. That is a distributed durability path, not a configuration tweak.

Neon's pageserver behavior — where page versions are materialized from WAL history and returned to compute on demand while object storage stays behind the pageserver rather than in front of query execution — is storage-engine behavior. PgBouncer does not touch it.

Branch-like workflows with snapshots or clones approximate part of Neon's documented storage timeline model, but the approximation is rough. The storage architecture is not portable to a stock single-node Postgres deployment, and describing it as such would be misleading.


FAQ

Should I use session pooling instead of transaction pooling to avoid the compatibility issues?

Session pooling is simpler operationally because it preserves session state between queries, but it provides much weaker connection multiplexing. Under bursty traffic, session pooling can accumulate idle server connections quickly. Neon uses transaction pooling specifically because modern application traffic has many short-lived transactions, not long sessions. Use session pooling for tools that require it, and transaction pooling for application traffic.

What pool size should I start with?

Neon's current documented formula is default_pool_size = 0.9 * max_connections. Start there and adjust down if you see queue buildup or resource pressure. A smaller active pool with predictable latency is more useful than a large pool with variable queue times.

Do I need two separate Postgres instances for pooled and direct access?

No. Two connection strings pointing to the same Postgres instance work fine: one routes through PgBouncer in transaction mode, the other connects directly to Postgres. The split is at the application configuration layer, not the infrastructure layer.

Will this approach help with serverless or edge function traffic?

Yes, this is the primary use case where the model provides the most leverage. Serverless functions open and close connections on every invocation. Without a pooler absorbing that churn, each invocation creates a new Postgres backend. With PgBouncer in transaction mode, many invocations share a small set of real server connections.

How do I verify that migrations are actually using the direct connection?

Check your migration tooling's DSN configuration explicitly. Many frameworks default to the primary DATABASE_URL. Confirm that the variable pointing to the direct connection is being picked up by your migration runner, and verify by checking pg_stat_activity — migration sessions should appear with a direct connection rather than via the pooler.


Conclusion

Neon is often described as "just faster Postgres." That framing misses what is actually different. Neon changed the boundary between execution and durability, kept object storage off the hot query path, and used PgBouncer to absorb modern connection churn. Those three changes together explain why it behaves differently from a conventional hosted Postgres instance under connection-heavy workloads.

For self-hosters, the highest-return lesson is to copy the connection model first. Use PgBouncer in transaction pooling mode for app traffic, maintain a direct path for session-sensitive work, run Postgres on fast local storage, and measure real backend concurrency rather than headline connection count. That will not reproduce Neon in full. It will reproduce the part of Neon that most application teams actually benefit from day to day: less wasted backend capacity, better tolerance for connection churn, and cleaner operational boundaries between different workflow types.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija


Sources

  • Neon architecture overview
  • Neon connection pooling docs
  • Neon monitoring docs
  • Neon changelog, January 10, 2025
  • Neon public GitHub repository
  • PgBouncer features documentation
  • PgBouncer configuration documentation
📄View markdown version
0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

No comments yet

Be the first to share your thoughts on this post!

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Table of Contents

  • What Neon's Architecture Actually Changes, and What You Can Copy in a Self-Hosted Setup
  • The Core Architectural Move
  • Why Neon Behaves Differently Under Connection-Heavy Workloads
  • What to Actually Measure
  • The Part Most Self-Hosters Should Copy First
  • What Transaction Pooling Buys You, and What It Breaks
  • A Practical Self-Hosted Blueprint
  • What You Cannot Cheaply Copy
  • FAQ
  • Conclusion
  • Sources
On this page:
  • What Neon's Architecture Actually Changes, and What You Can Copy in a Self-Hosted Setup
  • The Core Architectural Move
  • Why Neon Behaves Differently Under Connection-Heavy Workloads
  • What to Actually Measure
  • The Part Most Self-Hosters Should Copy First