Server architecture basics: what actually happens between DNS and your code
You cannot debug what you cannot picture. This is the map of a production request, layer by layer.
PostgreSQL uses one operating system process per connection, so connections are expensive in memory and scheduling. Application pools must be sized so that instances × pool size stays under max_connections; beyond a handful of instances or in serverless environments you need an external pooler like PgBouncer in transaction mode.
Each connection forks a backend process with its own memory for work_mem, temporary buffers and catalog caches — typically several megabytes at rest, more under query load. Two hundred idle connections can consume gigabytes and add scheduler pressure before executing a single query.
This is different from databases that use a thread-per-connection or event-driven model. It is why PostgreSQL advice about connection counts sounds conservative compared to what people expect.
Start from the database's capacity, divide by the number of application instances, and leave headroom for maintenance and scaling events.
max_connections = 100 (database setting)
reserved for superuser = 3
reserved for migrations = 5
available to applications = 92
instances (steady state) = 6
instances (peak autoscale) = 10
safe pool size per instance = floor(92 / 10) = 9A frequently-cited starting point for the total active connection count is around two to four times the number of CPU cores available to the database. Beyond that, queries queue inside PostgreSQL instead of in your pool, which is strictly worse because you lose visibility and control.
| Pool mode | Connection released | Safe for |
|---|---|---|
| Session | When the client disconnects | Everything; least efficient |
| Transaction | At transaction end | Most web apps; the usual choice |
| Statement | After each statement | Autocommit-only workloads; rarely used |
Anything that assumes the same backend across statements. Because a different physical connection can serve your next statement, session-scoped features become unreliable.
SELECT state, count(*),
max(now() - state_change) AS longest
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state;Up to a point, but each connection costs memory and scheduling. Raising it to 1000 on a 4-core machine converts a connection error into a much slower database. Pool instead.
Application code that opens a transaction and then does slow work — an HTTP call, file processing — before committing. Keep transactions short and never do I/O inside one.
Many now offer a built-in pooler, and some serverless drivers connect over HTTP to avoid the problem entirely. Check what your provider offers before deploying your own PgBouncer.
It removes connection setup cost and prevents overload, which improves latency under concurrency. It does nothing for an unindexed query — that is a separate problem.
ROVQIX Engineering
Engineering team, ROVQIX
The ROVQIX engineering team builds and maintains web platforms, APIs and infrastructure for clients across SaaS, ecommerce and enterprise. These notes come out of real production work — deploys, incidents, migrations and audits.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
You cannot debug what you cannot picture. This is the map of a production request, layer by layer.
Everyone runs EXPLAIN. Fewer people read the row estimates, which is where the actual answer usually is.
Read Committed prevents dirty reads. It does not prevent two people spending the same balance — and that is the bug you will get.
No spam. Just the occasional case study and craft breakdown.