DEV Community

Vigilmon
Vigilmon

Posted on

Uptime Monitoring for Render.com Services (Free, Multi-Region)

Uptime Monitoring for Render.com Services (Free, Multi-Region)

Render is one of the most popular Heroku alternatives for good reasons: Git-push deploys, managed Postgres, Redis, and background workers all in one place. The free tier lets you ship a full-stack app for $0.

But Render's free tier has a well-known catch: web services spin down after 15 minutes of inactivity. The first request after spin-down takes 30–60 seconds to wake. And Render doesn't email you when this happens — you just get customer complaints.

This guide shows you how to add a health check, set up external monitoring, and distinguish between "service is sleeping" and "service is broken."


Failure modes on Render

Free tier spin-down — The service is "up" from Render's perspective (it will restart), but unavailable to users for 30–60 seconds. Your HTTP monitor will see a timeout, which is functionally the same as downtime for your users.

Failed deploys with no rollback — Unlike Railway and Heroku, Render on the free tier doesn't automatically roll back on a failed build if the build succeeds but the app crashes on startup. You deploy, the build passes, the app crashes, and Render loops retrying the start command. Requests fail.

Background worker crashes — Render background workers run separately from web services. If your worker crashes and you don't have a health check for it, you won't know it stopped processing jobs until the queue backs up.

Managed Postgres connection limits — Render's free Postgres instances have strict connection limits. Connection pool exhaustion is silent — queries just hang.


Step 1: Add a /health endpoint

# For Flask
from flask import Flask, jsonify
import psycopg2
import os
import time

app = Flask(__name__)

def check_database():
    start = time.time()
    try:
        conn = psycopg2.connect(os.environ['DATABASE_URL'], connect_timeout=3)
        cursor = conn.cursor()
        cursor.execute('SELECT 1')
        cursor.close()
        conn.close()
        return {'status': 'ok', 'latency_ms': round((time.time() - start) * 1000)}
    except Exception as e:
        return {'status': 'error', 'error': str(e)}

@app.route('/health')
def health():
    db = check_database()
    all_ok = db['status'] == 'ok'

    response = {
        'status': 'ok' if all_ok else 'degraded',
        'checks': {'database': db},
        'render_service': os.environ.get('RENDER_SERVICE_NAME', 'unknown'),
        'render_instance': os.environ.get('RENDER_INSTANCE_ID', 'unknown'),
    }

    return jsonify(response), 200 if all_ok else 503
Enter fullscreen mode Exit fullscreen mode
// For Express
const express = require('express')
const { Pool } = require('pg')

const app = express()
const pool = process.env.DATABASE_URL
  ? new Pool({ connectionString: process.env.DATABASE_URL, max: 5 })
  : null

app.get('/health', async (req, res) => {
  const checks = {}

  if (pool) {
    const start = Date.now()
    try {
      await pool.query('SELECT 1')
      checks.database = { status: 'ok', latencyMs: Date.now() - start }
    } catch (err) {
      checks.database = { status: 'error', error: err.message }
    }
  }

  const allOk = Object.values(checks).every(c => c.status === 'ok')

  res.status(allOk ? 200 : 503).json({
    status: allOk ? 'ok' : 'degraded',
    service: process.env.RENDER_SERVICE_NAME ?? 'unknown',
    checks,
  })
})
Enter fullscreen mode Exit fullscreen mode

Render injects RENDER_SERVICE_NAME and RENDER_INSTANCE_ID — use them to make your health response self-describing.


Step 2: Add a background worker health endpoint

Render background workers don't serve HTTP by default, but you can add a lightweight status endpoint:

# worker.py — background worker with a side HTTP health server
import threading
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
import json

worker_status = {
    'last_job_at': None,
    'jobs_processed': 0,
    'errors': 0,
}

class HealthHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/health':
            status = 'ok'
            # Flag as degraded if no job processed in last 5 minutes
            if worker_status['last_job_at']:
                idle_seconds = time.time() - worker_status['last_job_at']
                if idle_seconds > 300:
                    status = 'idle_warn'

            body = json.dumps({
                'status': status,
                **worker_status,
            }).encode()
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(body)

    def log_message(self, *args):
        pass  # Suppress access logs

def start_health_server():
    server = HTTPServer(('0.0.0.0', 8080), HealthHandler)
    server.serve_forever()

# Start health server in background thread
threading.Thread(target=start_health_server, daemon=True).start()

# Main worker loop
def process_job(job):
    worker_status['last_job_at'] = time.time()
    worker_status['jobs_processed'] += 1
    # ... do real work here
Enter fullscreen mode Exit fullscreen mode

Point a separate monitor at this worker's health endpoint.


Step 3: Prevent free-tier spin-down with monitoring pings

Here's the clever part: if your external monitor checks your service every 60 seconds, it prevents Render from spinning it down. You get two benefits for the price of one — no spin-down AND you know immediately when the service is actually broken.

Set your monitor interval to 60 seconds and Render's free service stays warm 24/7.


Step 4: Set up external monitoring

  1. Go to vigilmon.online — free tier, no credit card.
  2. Create an HTTP(S) monitor.
  3. URL: https://your-service.onrender.com/health
  4. Interval: 60s
  5. Expected status: 200
  6. Timeout: 60s (Render spin-ups can be slow; give it time before declaring failure)
  7. Regions: at least two
  8. Alerts: email and/or Slack

For background workers, create a second monitor pointing at port 8080 (or whichever port you chose for the health server).


Step 5: Distinguish spin-down from failure

A 30–60 second response time is a spin-down. A 60+ second timeout or a 503 is a real problem. Configure your alerts accordingly:

  • Latency alert > 5000ms: service is waking from spin-down — informational
  • Status 503 or timeout: service is actually broken — page the on-call

Recap

  1. Add /health endpoint using Render's injected env vars for self-describing responses.
  2. Add a background thread health server to monitor workers, not just web services.
  3. Use a 60-second monitor interval — it doubles as a keep-alive for the free tier.
  4. Set a generous timeout (60s) on the monitor for spin-up periods.
  5. Create separate monitors for web services and background workers.
  6. Use vigilmon.online for the external monitoring — free, multi-region.

The free tier is great for side projects. Add external monitoring and it becomes great for side projects that you actually know about when they break.

Top comments (0)