Guide · Render
MCP server on Render
Render's Web Service is a good fit for HTTP/SSE MCP servers: you get free TLS, a permanent URL, a built-in health check mechanism, and managed Redis/Postgres. Three things require explicit attention: Render binds on port 10000 by default (not the standard 3000), its health check path controls zero-downtime deploy behavior, and its "free tier" instances spin down after inactivity — a cold start on the first MCP request can take 30–60 seconds, which breaks most MCP clients. Use at least the Starter plan for any server that clients expect to be always-available.
TL;DR
Deploy as a Render Web Service with HTTP/SSE transport. Bind to process.env.PORT (Render sets this to 10000 on paid plans). Add a render.yaml Blueprint to version-control your service config. Set healthCheckPath: /healthz in render.yaml — Render sends traffic to the new instance only after it passes health checks, giving you zero-downtime deploys. Mount a persistent disk at /data for SQLite. Use Render's private network (redis://<service>:6379) for Redis connections. Monitor the public endpoint with AliveMCP to catch protocol-level failures that Render's health checks don't detect.
render.yaml Blueprint
A render.yaml at the project root defines your services as code. This is the complete configuration for an MCP server with a persistent disk:
services:
- type: web
name: mcp-server
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: node dist/index.js
healthCheckPath: /healthz
envVars:
- key: NODE_ENV
value: production
- key: DB_PATH
value: /data/mcp.db
- key: REDIS_URL
fromService:
type: redis
name: mcp-redis
property: connectionString
disk:
name: mcp-data
mountPath: /data
sizeGB: 1
- type: redis
name: mcp-redis
plan: starter
ipAllowList: [] # private network only
The ipAllowList: [] on the Redis service blocks all public access — it's only reachable via Render's private network from your web service. The fromService.connectionString reference automatically injects the correct internal Redis URL into the web service's environment.
Port binding and the 10000 default
Render's Web Service routes external HTTPS traffic to your process on port 10000 by default. Like Railway, you must bind to process.env.PORT — not a hardcoded value. Render sets PORT=10000 in the environment:
import express from 'express';
const app = express();
const PORT = parseInt(process.env.PORT || '10000', 10);
// SSE transport setup
const transports: Record<string, SSEServerTransport> = {};
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/messages', res);
transports[transport.sessionId] = transport;
res.on('close', () => delete transports[transport.sessionId]);
await server.connect(transport);
});
app.post('/messages', async (req, res) => {
const { sessionId } = req.query as { sessionId: string };
const transport = transports[sessionId];
if (!transport) return res.status(404).json({ error: 'Session not found' });
await transport.handlePostMessage(req, res);
});
app.listen(PORT, '0.0.0.0');
Health check and zero-downtime deploys
Render's health check determines when a new deploy is considered successful. With healthCheckPath set, Render sends HTTP GET requests to that path after starting the new instance. If the endpoint returns 200, Render switches traffic to the new instance and terminates the old one. If it doesn't return 200 within the timeout, Render rolls back to the previous instance automatically.
This gives you zero-downtime deploys as long as your health check endpoint correctly validates the MCP layer:
app.get('/healthz', async (req, res) => {
try {
// Validate that the MCP SSE endpoint is up
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 4000);
const resp = await fetch(`http://localhost:${process.env.PORT}/sse`, {
signal: controller.signal
});
clearTimeout(timeout);
if (!resp.ok) {
return res.status(503).json({ status: 'unhealthy', reason: 'sse_not_ok' });
}
// Verify database is accessible
const row = db.prepare('SELECT 1 AS ok').get();
if (!row) throw new Error('db not responding');
res.status(200).json({ status: 'ok' });
} catch (err) {
res.status(503).json({ status: 'unhealthy', error: (err as Error).message });
}
});
Set the health check timeout in render.yaml to match your server's initialization time. A TypeScript MCP server that initializes database connections and warms up caches may take 5–10 seconds. Render's default timeout is 30 seconds — adequate for most servers, but increase it if your initialization is slower.
Persistent disk for SQLite
Render Web Services have an ephemeral filesystem like all container-based platforms. The disk field in render.yaml mounts a persistent volume at the specified path. Your SQLite database, file-based caches, and any other durable state should live under this mount:
import Database from 'better-sqlite3';
const DB_PATH = process.env.DB_PATH ?? '/data/mcp.db';
const db = new Database(DB_PATH);
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('foreign_keys = ON');
Render's persistent disks survive service restarts and deploys. They don't survive service deletion. Disk I/O on Render disks is network-attached (similar to AWS EBS) — journal_mode = WAL significantly improves concurrent read performance on network-attached storage.
One disk per service: if you need to share data between two Render services, use Postgres or Redis instead of a shared disk. Render doesn't support mounting one disk into multiple services.
Private network between services
Services within the same Render project communicate over Render's private network without going through the public internet. Use the service name as the hostname:
# For a Redis service named "mcp-redis" in the same Render project:
REDIS_URL=redis://mcp-redis:6379
# For a Postgres service named "mcp-db":
DATABASE_URL=postgres://user:pass@mcp-db/mcpserver
Private network connections don't use TLS (traffic stays within Render's datacenter network), which is why exposing the Redis service publicly requires extra ipAllowList configuration. Internal traffic between services is private by default.
Free tier and spin-down behavior
Render's free tier spins down Web Services after 15 minutes of inactivity. The next incoming request triggers a cold start that takes 30–60 seconds. For an MCP client, this means the initialize request times out — most MCP clients have a 10–30 second timeout and will report a connection failure.
For development and testing, free tier is fine. For any server that clients expect to be reliably available, use at least the Starter plan ($7/month), which keeps the instance always running. The plan: starter field in render.yaml enforces this in version-controlled config — a reviewer can catch a plan regression before it ships.
External monitoring beyond Render's health checks
Render's health checks verify HTTP 200 from inside Render's network. They don't verify DNS resolution, TLS certificate validity, or MCP protocol correctness from outside Render. A lapsed TLS certificate causes all MCP client connections to fail while Render's own health checks report the service as healthy.
Add your Render service URL to AliveMCP for external protocol probing. AliveMCP runs the full initialize → tools/list sequence over HTTPS from an external vantage point — the same path your MCP clients take. Alerts fire when the protocol layer fails, not just when the process crashes. See MCP server health checks for how to combine internal and external probing.
Related questions
Render vs Railway — which should I use for an MCP server?
Both work well. Railway's UI is more polished and the nixpacks auto-detection reduces configuration. Render's render.yaml Blueprint is more explicit and integrates better with GitOps workflows. Railway pricing is usage-based (better for variable load); Render Starter is fixed monthly (more predictable). Render's persistent disks are slightly simpler to configure than Railway volumes for SQLite use cases. For teams that want infrastructure-as-code from day one, Render's Blueprint approach wins; for solo developers who want to ship fast, Railway's UI is quicker to start with.
Does Render support background workers for async MCP tool processing?
Yes. Render's Background Worker service type runs a process that doesn't bind to a port — no health checks, no HTTP routing. Use a background worker for long-running tool executions that you want to decouple from the MCP connection lifecycle: the MCP server enqueues the job (e.g., to Redis), the background worker picks it up and writes results back. This pattern prevents long tool calls from blocking the MCP connection. See async tool patterns in MCP servers for the queue-based decoupling approach.
How do I handle Render's 55-second request timeout for SSE streams?
Render's HTTP proxy has a 55-second idle timeout on connections. For long-running SSE streams, configure your server to send a keepalive comment (: keepalive\n\n) on the SSE stream every 30 seconds. The SSE spec allows comment lines for exactly this purpose — they're ignored by clients but reset the proxy's idle timeout. Most MCP clients also handle disconnection and reconnection gracefully, so a clean disconnect after the timeout is often acceptable.
Further reading
- MCP server deployment — transport selection and rolling-restart safety
- MCP server Docker — Dockerfile and signal handling
- MCP server health checks — the full initialize probe sequence
- MCP server on Railway — nixpacks, env vars, and health checks
- MCP server on Fly.io — regions, volumes, and machine restarts
- MCP server zero-downtime deployment
- MCP server observability — metrics, tracing, and external probing
- AliveMCP — external monitoring for your Render-hosted MCP server