ai-architecture
ranked
05 Sept 2026
Six ways to reach Postgres from a serverless function, ranked by what breaks first
The connection string the dashboard hands you is the one most likely to fail from a serverless host. Here is what each route actually does, and the error you get when it is the wrong one.
By Purvansh Parmar
Checked 2026-09-05
Read 8 min
Entries 6
A serverless function opens a fresh connection on every cold start, then abandons it without warning when the platform freezes the container mid-request. Postgres was built for the opposite. It expects a handful of long-lived clients that say goodbye properly, hold their session state between statements, and close when they are finished, which is the exact opposite of what a function does. Almost every problem below comes from that mismatch. None of them announce it.
Ordered by how early each option fails, not by preference. The first two stop you before you write a query.
How this was checkedI wired a Netlify function to Supabase today and hit four of these six in one afternoon. The direct host returned connect ENETUNREACH to an IPv6 address on the first attempt. The pooler accepted the same credentials immediately. A password reset was rejected for about two minutes before the pooler accepted it, which reads exactly like a wrong password if you do not know to wait. The REST client then threw at import time on the function host while working on my laptop, because the runtime pin and my local Node differed by two major versions.
What I compared on
- does it connect at all from an IPv4-only host
- what it does with prepared statements
- what the failure looks like when it is wrong
- how much it drags into the bundle
01 Direct connection, port 5432
The connection string the dashboard shows first, straight to the database host.
Wins
It is the only route that supports session-level features: LISTEN and NOTIFY, advisory locks, temporary tables, and prepared statements without ceremony. For a migration run from a laptop it is the correct choice and the simplest one.
Loses
Supabase serves this host over IPv6 only unless the IPv4 add-on is paid for. Netlify Functions egress over IPv4, so the connection never opens. The error is connect ENETUNREACH followed by an IPv6 address, which reads like a network outage rather than a design decision.
port 5432IPv6 onlyENETUNREACH
02 Transaction pooler, port 6543
Supavisor in transaction mode, handing out a backend connection per transaction.
Wins
Reachable over IPv4 and built for exactly this shape of client: thousands of short-lived callers that each want one statement. It absorbed a function opening a connection per cold start without any tuning.
Loses
Prepared statements break. Each transaction can land on a different backend, so a statement prepared on one is missing on the next, and queries start failing intermittently rather than immediately. Drivers need prepare disabled explicitly, and the symptom if you forget looks like a flaky database.
port 6543prepare: false
03 Session pooler, port 5432 on the pooler host
The same pooler holding one backend connection for the life of your session.
Wins
IPv4 reachable like the transaction pooler, but session semantics survive, so prepared statements and session settings behave the way the driver expects. It is the low-friction answer for a long-running process that cannot reach the direct host.
Loses
A held connection is the thing serverless is worst at. Every frozen container keeps a backend occupied until it times out, so the pool drains under exactly the traffic pattern taht makes you want serverless. Good for a container, wrong for a function.
port 5432one backend per session
04 PostgREST over HTTPS
Skip the wire protocol entirely and talk to the REST layer the database already exposes.
Wins
No connection to manage, no pool to exhaust, no port to be unreachable. Row level security is enforced by the database for every request, so a read path cannot leak rows a policy forbids even if the query forgets to filter.
Loses
Anything needing one transaction across several statements has to move into a database function, because two HTTP calls can half-fail. That is a real loss: the logic leaves the repository and lives somewhere git cannot review it.
0 connections1 round trip per call
One client object covering database, auth, storage and realtime.
Wins
The ergonomics are genuinely good, and using one library across a browser app and a server function means one mental model. Schema selection, filters and RPC calls read cleanly.
Loses
It builds subsystems you did not ask for. Creating a client constructs a realtime client whether or not you use realtime, and that needs a global WebSocket, which Node only gained in version 22. On a host pinned to Node 20 every function throws at import time, before any of your code runs.
Node 22 requiredthrows at import
The same query builder as the full SDK, without auth, storage or realtime attached.
Wins
It does one job, so it cannot fail on a subsystem you never wanted. For a server-side function that only reads and writes rows this is the smallest correct dependency, and it carries no runtime version coupling.
Loses
You set the auth headers yourself, and you give up the parts of the SDK that are genuinely convenient elsewhere. If the same codebase also runs in a browser doing auth, you now have two libraries where one would have done.
1 subsystem2 headers to set by hand
Side by side
| Route | IPv4 | Prepared statements | Fails with |
| Direct 5432 | no | yes | ENETUNREACH |
| Transaction pooler 6543 | yes | no | intermittent query errors |
| Session pooler 5432 | yes | yes | pool exhaustion |
| PostgREST | yes | n/a | nothing, until you need a transaction |
| Vendor SDK | yes | n/a | missing WebSocket at import |
| Query layer only | yes | n/a | nothing |
Verdict
Use PostgREST over HTTPS. I moved the one multi-statement write into a database function, which cost a stored procedure but bought a read path that row level security enforces for me whether or not some future query remembers to filter. If you need the wire protocol, take the transaction pooler and turn prepared statements off. Reach for the direct connection only when you are running a migration from a machine you control. And check the address family before you decide the network is broken.
Sources