How to connect a Render service to Supabase Postgres
If your Render service can't reach its Supabase database on the first deploy, or it connects and then fails once traffic grows, check the connection string first. This guide covers three things:
- Picking a Supabase connection mode for each Render workload.
- Fixing the IPv6 problem behind many failed first connections.
- Sizing connection pools so autoscaling doesn't exhaust your database.
Prerequisites: You already run a service on Render and a database on Supabase. This guide doesn't cover creating either one.
Short answer: Most Render services should use Supabase's shared pooler.
- Use transaction mode (port 6543) for request/response traffic and for short, bursty jobs.
- Use session mode (port 5432) for long-lived processes that depend on session state.
- Use the direct connection from Render only if your Supabase project has the paid IPv4 add-on.
The sections below explain how each mode handles connections, then map each Render workload to a mode.
How Supabase connection modes differ
Supabase gives you four ways to connect. Each one handles connections differently, and that difference drives every recommendation in this guide.
Mode | Port | Plan availability | Notes |
|---|---|---|---|
Direct connection | 5432 | All plans | IPv6 unless you buy the paid IPv4 add-on. Preserves session state and prepared statements. |
Shared pooler, session mode | 5432 | All plans | Preserves session state for the life of the client's session. |
Shared pooler, transaction mode | 6543 | All plans | The connection returns to the pool after each transaction. No prepared statements. |
Dedicated pooler | 6543 (on the database host, db.[PROJECT-REF].supabase.co) | Paid plans only | Transaction mode only. IPv6 unless you buy the IPv4 add-on. |
Session mode and the direct connection both use port 5432, so check the host as well as the port when you copy a string. Every mode follows the same Postgres URL pattern:
Set the string you choose as an environment variable, such as DATABASE_URL, on your Render service.
Pick a connection mode for each Render workload
Each row matches one of Render's service types to the Supabase connection string that fits how that workload uses connections.
Render workload | Recommended Supabase mode | Port | Why | Watch out for |
|---|---|---|---|---|
Web service | Shared pooler, transaction mode. Use session mode if your app depends on session-level features. | 6543 (session: 5432) | Requests run short queries and release the connection quickly. | Prepared statements in transaction mode. Every instance's pool counts against your connection budget. |
Background worker | Shared pooler, session mode. Use the direct connection only with the IPv4 add-on. | 5432 | A long-running process benefits from keeping session state across queries. | Render can reset long-lived outbound connections to external databases, so add reconnect logic. |
Cron job | Shared pooler, transaction mode. | 6543 | Short, bursty runs shouldn't hold connections between queries. | Render runs at most one instance of a given cron job at a time, so you budget for one run's pool. |
Autoscaled service | Shared pooler, transaction mode with a small per-instance pool. | 6543 | The instance count changes, so the total number of connections changes with it. | Size the pool against your maximum instance count. See the budget math below. |
On paid Supabase plans, you can use the dedicated pooler instead of the shared pooler for transaction-mode traffic. Like the direct connection, it is IPv6 unless your project has the IPv4 add-on, so Render can reach it only with that add-on.
Choose the mode by how your code uses a connection, not by how important the service is. If your code holds a connection and relies on state tied to it, use session mode. If it borrows a connection, runs a query or transaction, and returns it, use transaction mode.
Fix the IPv6 trap behind failed first connections
A frequent cause of a failed first connection has nothing to do with credentials. It follows this chain:
- Supabase's direct connection (the direct database host on port 5432) uses IPv6 unless your project has the paid IPv4 add-on.
- Render doesn't support outbound IPv6, so a Render service can't reach an IPv6-only host.
- As a result, the direct connection string fails from Render by default, even when your credentials are correct.
To confirm the diagnosis, check what your database host resolves to. This minimal example runs dig, a standard DNS lookup tool, from your own machine:
Symptom | Likely cause | Fix |
|---|---|---|
The connection times out or fails to resolve on the first deploy | The direct host resolves only to an IPv6 address | Fix A or Fix B |
The connection works from your laptop but fails from Render | Your local network routes IPv6, but outbound traffic from Render doesn't | Fix A or Fix B |
You have two fixes:
- Fix A: Switch to the shared pooler connection string, in session mode on port 5432 or transaction mode on port 6543. The shared pooler is available on all Supabase plans, and most workloads in the decision table use it anyway.
- Fix B: Buy Supabase's IPv4 add-on, available on Pro plans and above, to keep using the direct connection. Afterward, run the
digcheck again to confirm the host returns an A record.
Avoid prepared statements in transaction mode
Transaction mode doesn't support prepared statements. A prepared statement belongs to the database session that created it, and PostgreSQL keeps it only for the duration of that session (PostgreSQL PREPARE docs). In transaction mode, the pooler can hand your next transaction a different server connection, so a statement you prepared earlier might not exist there. The resulting errors are intermittent and often appear only under load, once the pool starts rotating connections.
To avoid these errors, turn off server-side prepared statements or statement caching in your driver or ORM. Apply the setting only to services that use the transaction-mode string. The following simplified examples show the relevant option for several common Postgres clients. Adapt each one to how your app builds its connection.
asyncpg (Python). Disable the statement cache (asyncpg docs):
psycopg 3 (Python). Disable automatic preparation (psycopg prepared statements docs):
Postgres.js (Node.js, also used by Drizzle). Disable prepared statements in the client options (Postgres.js README):
Prisma. Add the pgbouncer=true parameter to the transaction-mode URL:
Prisma Migrate doesn't work through a transaction pooler (Prisma's PgBouncer docs), so give the Prisma CLI a separate DIRECT_URL. From Render, point DIRECT_URL at the shared pooler in session mode on port 5432, because the direct connection fails without the IPv4 add-on. Reference it as the datasource URL in prisma.config.ts with env("DIRECT_URL"), the setup Prisma's PgBouncer docs use for CLI commands.
If your app needs prepared statements, point that service at session mode on port 5432 instead.
Do the connection budget math before you scale
Connection exhaustion at scale starts with one sum per mode:
Client connections for a mode = (maximum instances × pool size per instance), summed across the Render services that use that mode. Transaction-mode services count against the pooler's client limit. Session-mode and direct services count against the database's connection limit.
Here's how each limit works:
- Session mode and the direct connection: Session-mode clients on the shared pooler each hold one of the pooler's backend connections for as long as the client keeps it open, and those backend connections count against the database's connection limit. Direct connections count against the database's connection limit on their own.
- Transaction mode: The pooler multiplexes many client connections onto a smaller set of database connections. These client connections count against the pooler's client limit. Supabase sets both the direct connection limit and the pooler client limit by compute size. For example, the Small compute size allows 90 direct connections and 400 pooler clients. Find your project's limits in Supabase's compute size table. The pooler's own backend connections count against the database limit.
For a deeper walkthrough of how a transaction pooler behaves when many clients open connections at once, see 200 concurrent task runs meet your Postgres connection limit.
To see the database's limit, run SHOW max_connections; in a SQL session (PostgreSQL connection settings).
Size against the maximum number of instances your autoscaled service can reach, not the typical number. Exhaustion happens at peak, when new instances start and each one opens its own pool. Workers and cron jobs draw from the same budget, so include them.
Here's a worked example with placeholder numbers that follows the decision table's modes. Substitute your own service settings and limits.
Render service | Mode | Max instances | Pool per instance | Client connections |
|---|---|---|---|---|
Web service (autoscaled) | Transaction | 8 | 5 | 40 |
Cron job | Transaction | 1 | 2 | 2 |
Background worker | Session | 2 | 5 | 10 |
The two transaction-mode services open 42 client connections, and that total must fit the pooler's client limit. The worker's 10 session-mode clients each hold one of the shared pooler's backend connections. Direct connections plus the pooler's backend connections, plus headroom for migrations and admin tools, must fit under max_connections. Past either limit, the pooler or the database refuses new connections. That usually hits the newest instances first, which are the ones that started to absorb a traffic spike.
If the numbers don't fit, you have these levers:
- Shrink the per-instance pool. Short request/response queries rarely need large pools.
- Move services to transaction mode. Connections return to the pool after each transaction instead of staying pinned to a client.
- Lower your autoscaling ceiling. Review your scaling settings so the maximum instance count fits the budget.
- Consider the dedicated pooler, available on paid Supabase plans. From Render it needs the IPv4 add-on, because it is IPv6 by default and Render doesn't support outbound IPv6.
Pair your Render region with your Supabase region
Run your Render service in the Render region closest to your Supabase project's region. Every query crosses the public internet between the two providers, so distance adds latency to each round trip. Code that makes several queries per request multiplies that cost.
Render services run in Oregon, Ohio, Virginia, Frankfurt, or Singapore. Choose the region carefully when you create the service, because Render doesn't currently support changing the region of an existing service.
Budget for Supabase egress
Query results that Supabase sends back to your Render service count as egress on Supabase's side. On Render's side, inbound bandwidth to your services is free. Per Supabase's documentation, the Pro plan includes 250 GB of egress. Past that quota, Supabase bills $0.09 per GB.
For a sense of scale, take a hypothetical service that pulls 400 GB from Supabase in a month. That's 150 GB over the quota, which adds 150 × $0.09 = $13.50. Large result sets, SELECT * queries, and polling loops drive this number up fastest.
Supabase's pricing can change, so confirm current rates on Supabase's pricing page before you budget.
Supabase prices checked between 2026-09-28 and 2026-10-20.
Know when to stop and colocate
A cross-provider setup is a reasonable starting point. Watch for these signals that it costs more than it's worth:
- Your egress spend keeps climbing as read traffic grows.
- Latency-sensitive endpoints stay slow even after you pair regions.
- You keep adjusting pool sizes and instance ceilings to stay under the connection limits.
Colocation changes the network path, because Render services in the same region reach each other without traversing the public internet. If these signals sound familiar, compare your setup against Render Postgres using the Render Postgres vs Supabase comparison. Base the decision on your own egress, latency, and connection numbers.
To structure that comparison, use the evaluation framework in how to choose a managed PostgreSQL provider in 2026. If your numbers point toward moving off Supabase entirely, Supabase alternatives for full-stack backends covers what that migration involves.
Next steps
- Check your current connection string. If it uses the direct host, switch to the shared pooler (Fix A) or add the IPv4 add-on (Fix B).
- Match each Render service to a row in the decision table and update its
DATABASE_URL. - Turn off prepared statements for every service that uses transaction mode on port 6543.
- Fill in the budget table with your real maximum instance counts, and check the totals for each mode against the right limit.
- Add reconnect logic with exponential backoff to workers and other long-lived processes, because Render can reset established outbound connections when routing changes.