tutorial

Monitoring Apps That Use Neon Serverless Postgres with Vigilmon

How to add database health checks to your API when using Neon (serverless Postgres), and wire them into Vigilmon for uptime monitoring and alerting on DB connection failures.

Neon is a serverless Postgres platform with autoscaling, database branching, and per-second billing. You don't manage servers, but your application still depends on a database connection — and Neon's serverless architecture introduces its own failure modes: cold computes that take a moment to wake, connection pool exhaustion, and branch mismatches between staging and production. Vigilmon can't monitor Neon directly (it's a managed service), but it can monitor your application and its ability to reach Neon — which is what actually matters for your users.

This tutorial shows you how to add a database health check to your API and use Vigilmon to alert you the moment your app loses its Neon connection.

What You'll Build

  • A /health API endpoint that probes the Neon Postgres connection
  • A Vigilmon HTTP monitor watching the health endpoint
  • Alerts specifically tuned for database connection failures and Neon cold starts

Prerequisites

  • An application using Neon Postgres (any language/framework)
  • Your app deployed to a reachable URL (Vercel, Render, Railway, a VPS, etc.)
  • A free account at vigilmon.online

Step 1: Add a DB Health Check to Your API

The health check should verify that your application can actually query Neon — not just that the HTTP server is running.

Node.js with pg or postgres

// routes/health.js
import postgres from "postgres";

const sql = postgres(process.env.DATABASE_URL, { max: 1 });

export async function healthHandler(req, res) {
  const checks = {};
  let ok = true;

  try {
    // Lightweight query — doesn't touch user tables
    const result = await sql`SELECT 1 AS db_ok`;
    checks.database = result[0]?.db_ok === 1 ? "ok" : "unexpected_result";
  } catch (err) {
    checks.database = `error: ${err.message}`;
    ok = false;
  }

  res.status(ok ? 200 : 503).json({
    status: ok ? "ok" : "degraded",
    checks,
  });
}

Node.js with Prisma

// routes/health.js
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export async function healthHandler(req, res) {
  const checks = {};
  let ok = true;

  try {
    await prisma.$queryRaw`SELECT 1`;
    checks.database = "ok";
  } catch (err) {
    checks.database = `error: ${err.message}`;
    ok = false;
  } finally {
    await prisma.$disconnect();
  }

  res.status(ok ? 200 : 503).json({ status: ok ? "ok" : "degraded", checks });
}

Python with psycopg2 or asyncpg

# routes/health.py
import os
import psycopg2
from flask import jsonify

def health():
    checks = {}
    ok = True

    try:
        conn = psycopg2.connect(os.environ["DATABASE_URL"], connect_timeout=5)
        with conn.cursor() as cur:
            cur.execute("SELECT 1")
        conn.close()
        checks["database"] = "ok"
    except Exception as e:
        checks["database"] = f"error: {str(e)}"
        ok = False

    return jsonify({"status": "ok" if ok else "degraded", "checks": checks}), 200 if ok else 503

Go with database/sql

// handlers/health.go
package handlers

import (
    "database/sql"
    "encoding/json"
    "net/http"
    "time"
)

func HealthHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        checks := map[string]string{}
        ok := true

        db.SetConnMaxLifetime(5 * time.Second)
        if err := db.PingContext(r.Context()); err != nil {
            checks["database"] = "error: " + err.Error()
            ok = false
        } else {
            checks["database"] = "ok"
        }

        status := "ok"
        code := http.StatusOK
        if !ok {
            status = "degraded"
            code = http.StatusServiceUnavailable
        }

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(code)
        json.NewEncoder(w).Encode(map[string]any{"status": status, "checks": checks})
    }
}

Wire this to GET /health in your router of choice.


Step 2: Handle Neon's Serverless Cold Start

Neon's serverless computes autosuspend after a period of inactivity (default: 5 minutes on the free plan). The first connection after suspension takes a few seconds while the compute wakes. This is expected behavior, but it can cause your health check to time out if your Vigilmon timeout is too tight.

Recommendations:

  • Set Vigilmon's response timeout to 15 seconds for apps on Neon's free tier.
  • Use Neon's connection pooler (-pooler connection string suffix) — it keeps connections warm independently of the compute.
  • Consider setting autosuspend to a longer delay or disabling it on Neon's Pro plan for production workloads.
# Standard connection string
DATABASE_URL=postgres://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb

# Pooler connection string (preferred for serverless apps)
DATABASE_URL=postgres://user:pass@ep-xxx-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require

Step 3: Configure the Vigilmon HTTP Monitor

  1. Log in to VigilmonAdd Monitor → HTTP.
  2. URL: https://your-app.example.com/health.
  3. Check interval: 60 seconds.
  4. Response timeout: 15 seconds (Neon cold start headroom).
  5. Expected status: 200.
  6. JSON assertion: path status, expected value ok.
  7. Click Save.

Tip: If your app is deployed to Vercel serverless functions, the /health endpoint itself may also cold-start. Set the timeout to 20 seconds to cover both the Vercel function startup and the Neon compute wakeup.


Step 4: Alert on Database Connection Failures

Configure Vigilmon alerts under Settings → Notifications:

| Trigger | Alert | Likely cause | |---|---|---| | HTTP 503 | Immediate email + Slack | Neon compute suspended, connection pool exhausted, or credentials rotated | | Response timeout | Immediate email | Neon cold start taking too long; increase autosuspend delay | | JSON statusok | Immediate email | Health check ran but DB query failed | | Monitor unreachable | Immediate email | App server is down, not a Neon issue |

Set alert after: 2 consecutive failures. A single slow cold-start wakeup shouldn't page on-call. Two consecutive failures almost always mean a real problem.


Step 5: Environment-Specific Monitors

Neon's branching feature means you likely have multiple database branches (production, staging, preview). Add a monitor per environment:

| Monitor | URL | Alert channel | |---|---|---| | Production /health | https://myapp.com/health | Email + Slack (on-call) | | Staging /health | https://staging.myapp.com/health | Slack only |

This catches the case where a bad migration on the staging branch (which may share the same Neon project) causes failures that could be confused with production issues.


What Vigilmon Catches That Neon's Dashboard Misses

| Scenario | Neon dashboard | Vigilmon | |---|---|---| | App can't connect to Neon | Shows compute active | HTTP monitor fires (app-side failure) | | Wrong branch in DATABASE_URL | No alert | Health check fails, Vigilmon fires | | Connection pool exhausted | Metrics may lag | Health endpoint returns 503, monitor fires | | App server is down (not Neon) | No visibility | HTTP monitor catches unreachable | | Cold start causes user-facing timeout | Compute logs | Response timeout fires alert |


Neon handles the database infrastructure, but it can't monitor your application's connection to it. That's your job — and Vigilmon makes it easy. One health endpoint, one monitor, and you'll know within 60 seconds if your app loses its database.

Add database monitoring to your Neon-backed app in minutes — register free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →