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 initializetools/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