Render
A platform-as-a-service for always-on containers — the counterpart to Vercel's serverless model. Push to git, Render builds a container and runs it as a long-lived process.
Pages
1. What Render Is
| Service type | What it is |
|---|---|
| Web Service | A long-running HTTP server (Node, Python, Go, Docker) |
| Static Site | Built assets on a CDN |
| Private Service | An internal HTTP service, not exposed publicly |
| Background Worker | A process with no HTTP port — queue consumers |
| Cron Job | A container that runs on a schedule and exits |
| Postgres | Managed database with automated backups |
| Key Value | Managed Redis-compatible store |
The important difference from Vercel: your process stays running. It holds state in memory, keeps database connections open, and can serve WebSockets.
2. Render vs Vercel
| Render | Vercel | |
|---|---|---|
| Model | Long-running container | Serverless functions + CDN |
| Process lifetime | Always on | Per request |
| WebSockets | Yes | No |
| Background workers | Yes | No (cron only) |
| Cron jobs | Yes | Yes |
| Managed Postgres / Redis | Built in | Partner integrations |
| Connection pooling | Straightforward — one process, one pool | Needs an external pooler |
| Cold starts | Only on the free tier | Yes |
| Next.js support | Good | Best in class |
| Global edge | Single region per service | Global by default |
| Pricing | Mostly flat per service | Usage-based |
The Common Architecture
Next.js frontend on Vercel + Node API and Postgres on Render. Best frontend DX, and a normal always-on backend sitting next to its database.
When Render Is the Right Choice
- WebSockets — chat, live collaboration, notifications
- Background job workers consuming a queue
- Long-running requests (reports, exports, video processing)
- You want a managed database next to your app, one bill, one dashboard
- Predictable flat pricing rather than usage-based
- Stateful in-memory work that serverless makes awkward
3. Free Tier Caveats
Worth knowing, because it surprises people:
- Free web services spin down after ~15 minutes of inactivity. The next request takes 30–60 seconds to wake the container
- Free Postgres instances expire after a limited period
- Limited build minutes and bandwidth
The spin-down makes the free tier unsuitable for anything a real user might hit, but fine for a demo or a portfolio project you can warm up before showing.
Do Not "Fix" It With a Pinger
Hitting your own free service every 10 minutes to keep it awake defeats the purpose of the tier and is against the spirit of the terms. If it must stay up, pay for the smallest paid instance.
4. Infrastructure as Code — render.yaml
services:
- type: web
name: api
runtime: node
region: oregon
plan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
healthCheckPath: /health
autoDeploy: true
envVars:
- key: NODE_ENV
value: production
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString
- key: JWT_SECRET
generateValue: true # Render generates a random value once
- key: REDIS_URL
fromService:
type: keyvalue
name: app-cache
property: connectionString
- type: worker
name: job-worker
runtime: node
buildCommand: npm ci && npm run build
startCommand: npm run worker
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString
- type: cron
name: nightly-cleanup
runtime: node
schedule: "0 3 * * *"
buildCommand: npm ci
startCommand: node scripts/cleanup.js
databases:
- name: app-db
plan: basic-256mb
postgresMajorVersion: "16"Why This Matters in an Interview
A render.yaml (a "Blueprint") is infrastructure as code: the whole stack is versioned in git, reviewable in a PR, and reproducible. Clicking things in a dashboard is not reproducible and nobody can review it.
Note fromDatabase and fromService — Render injects connection strings automatically, so credentials never appear in the file. And generateValue: true creates a strong random secret once, without you inventing or committing one.
5. The Deploy Lifecycle
git push
→ Render detects the change
→ runs buildCommand in a build container
→ builds an image
→ starts a new instance with startCommand
→ waits for healthCheckPath to return 200
→ shifts traffic to the new instance
→ shuts down the old instance (SIGTERM)Zero-Downtime Requires Two Things From You
1. A working health check. Render will not route traffic to an instance whose healthCheckPath fails, and will not kill the old one. Without it, traffic can hit a container that is still booting.
2. Graceful shutdown. Render sends SIGTERM to the old instance. Handle it, stop accepting new connections, finish in-flight requests, then exit. Otherwise every deploy drops requests.
process.on("SIGTERM", () => {
server.close(async () => {
await db.$disconnect();
process.exit(0);
});
setTimeout(() => process.exit(1), 10_000).unref();
});These two points are exactly what "how do you do zero-downtime deploys" is asking about.
6. Health Checks
app.get("/health", (req, res) => res.json({ status: "ok" })); // liveness
app.get("/ready", async (req, res) => { // readiness
try {
await db.$queryRaw`SELECT 1`;
res.json({ status: "ready" });
} catch {
res.status(503).json({ status: "not ready" });
}
});Which One To Point Render At
Use the liveness endpoint (/health) for healthCheckPath, not the readiness one.
If your health check queries the database and the database has a brief hiccup, Render marks a perfectly healthy container unhealthy and restarts it — during an outage, when restarting helps nothing and loses your warm state.
Keep the dependency-checking endpoint separate, for your monitoring to alert on.
Getting this distinction right is a strong signal. Most people wire the database check straight into the health endpoint and create a restart loop the first time the database blips.
7. Databases
Connection Strings
| Use | |
|---|---|
| Internal | From another Render service in the same region — faster, free, not exposed to the internet |
| External | From your laptop or outside Render — goes over the public internet |
Always use the internal URL for service-to-service traffic. It avoids egress, avoids public exposure, and is lower latency.
Backups
Paid Postgres plans include automated daily backups and point-in-time recovery.
Test the restore. A backup you have never restored is a hypothesis, not a backup. Saying this unprompted lands well — plenty of teams discover their backups were broken only when they needed them.
Connection Pooling on Render
Simpler than serverless. One long-running process means one pool, reused across all requests.
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });Still size it correctly: max × number of instances must stay under the plan's connection limit. Three instances × max: 20 = 60 connections; check that against your plan.
8. Scaling
Vertical
Bigger instance — more CPU and RAM. Simplest, and often enough.
Horizontal
More instances behind Render's load balancer.
The prerequisite: your app must be stateless.
| In-memory thing | Move it to |
|---|---|
| Sessions | Redis (Render Key Value) |
| Cache | Redis |
| Rate-limit counters | Redis |
| Uploaded files | S3 / R2 |
| Scheduled work | A cron service, not setInterval in the web process |
The last one catches people: setInterval in a web service with three instances runs the job three times. Use a Cron Job service, or a distributed lock.
Autoscaling
Paid plans can scale on CPU and memory targets. Set a sensible maximum — autoscaling with no ceiling turns a traffic spike, or a bug, into a large bill.
9. Common Problems
| Symptom | Cause |
|---|---|
| Deploy succeeds, service unhealthy | App not listening on process.env.PORT |
| Service unreachable | Bound to 127.0.0.1 instead of 0.0.0.0 |
| Free service slow on first request | Spun down after inactivity — 30–60s cold start |
| Requests dropped on every deploy | No SIGTERM handler |
| Health check restart loop | Health endpoint checks the database |
too many connections | Pool size × instance count exceeds the plan limit |
| Build works locally, fails on Render | Case-sensitive imports, or a build dependency in devDependencies |
| Cron job runs but does nothing | Cron containers exit when the command exits — the process must complete, not detach |
The Port Binding One
// ❌ Render cannot reach this
app.listen(3000, "127.0.0.1");
// ✅
app.listen(process.env.PORT || 3000, "0.0.0.0");Render assigns the port via PORT and routes to the container's external interface. Hardcoding the port or binding to localhost is the single most common first-deploy failure on any container platform — Render, Railway, Fly, Heroku, Cloud Run. Worth knowing for all of them.