DEV Community

Vigilmon
Vigilmon

Posted on

Uptime Monitoring for GraphQL APIs (Free, Multi-Region)

Uptime Monitoring for GraphQL APIs (Free, Multi-Region)

Here's the problem with monitoring GraphQL APIs: they always return 200 OK.

Send a malformed query? 200 OK with an errors array. Your resolver crashed? 200 OK with a partial data response and errors. The introspection schema changed and your client is sending invalid field names? Still 200 OK.

Standard HTTP uptime monitors that check for status 200 will tell you your GraphQL API is healthy when it's returning nothing but errors. This guide covers how to write real GraphQL health checks and set up monitoring that actually detects failures.


Why standard HTTP monitoring misses GraphQL failures

REST APIs communicate errors through HTTP status codes: 400, 404, 500. Monitoring tools look for these.

GraphQL has a different contract: the HTTP layer always returns 200. Errors live in the response body:

{
  "data": null,
  "errors": [
    {
      "message": "Cannot query field 'userId' on type 'User'",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

A monitor checking status == 200 thinks this is healthy. Your users are seeing nothing but error messages.


Step 1: Add a dedicated health query to your schema

Add a _healthCheck query that your monitoring uses:

# schema.graphql
type HealthStatus {
  status: String!
  version: String
  latencyMs: Int
  database: DependencyStatus
}

type DependencyStatus {
  status: String!
  latencyMs: Int
  error: String
}

type Query {
  # ... your actual queries
  _healthCheck: HealthStatus!
}
Enter fullscreen mode Exit fullscreen mode

Implement the resolver:

// resolvers/health.ts
import { db } from '../db'

export const healthCheckResolver = async () => {
  const start = Date.now()

  let dbStatus = { status: 'ok', latencyMs: 0 }
  try {
    const dbStart = Date.now()
    await db.raw('SELECT 1')
    dbStatus = { status: 'ok', latencyMs: Date.now() - dbStart }
  } catch (err: any) {
    dbStatus = { status: 'error', error: err.message }
  }

  return {
    status: dbStatus.status === 'ok' ? 'ok' : 'degraded',
    version: process.env.APP_VERSION ?? 'unknown',
    latencyMs: Date.now() - start,
    database: dbStatus,
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Add a REST health endpoint alongside GraphQL

GraphQL is great, but your uptime monitor needs an endpoint that communicates health via HTTP status codes. Add a /health REST route even if the rest of your API is GraphQL:

// In your Express/Fastify/etc. setup
app.get('/health', async (req, res) => {
  // Run the same health check logic
  const result = await healthCheckResolver()
  const isOk = result.status === 'ok'

  res.status(isOk ? 200 : 503).json({
    status: result.status,
    checks: { database: result.database },
  })
})
Enter fullscreen mode Exit fullscreen mode

This is the endpoint your uptime monitor hits. The _healthCheck query is for internal tooling and dashboards.


Step 3: Monitor for errors in GraphQL responses

If you can't add a REST endpoint, you can configure your monitor to send a GraphQL request and check the response body. Here's how to do it with a custom HTTP monitor that sends a POST:

Configure the monitor to POST:

POST https://api.example.com/graphql
Content-Type: application/json

{"query":"{ _healthCheck { status } }"}
Enter fullscreen mode Exit fullscreen mode

Expected response body should contain "status":"ok" and NOT contain "errors".

Many monitoring tools support body assertions. With Vigilmon, you can:

  1. Set the request method to POST
  2. Set the request body to the introspection/health query
  3. Add a body contains assertion: "status":"ok"
  4. Add a body not contains assertion: "errors"

Step 4: Monitor schema integrity

Schema breaking changes are a silent killer. If you deploy a schema change that removes a field a client was querying, those clients start getting errors. Detect this early with a dedicated operation:

// scripts/schema-smoke-test.ts
import fetch from 'node-fetch'

async function smokeTestSchema() {
  const criticalOperations = [
    { name: 'UserQuery', query: '{ user(id: "test") { id email createdAt } }' },
    { name: 'ProductList', query: '{ products(first: 1) { edges { node { id title } } } }' },
    { name: 'HealthCheck', query: '{ _healthCheck { status } }' },
  ]

  const results = []
  for (const op of criticalOperations) {
    const res = await fetch(process.env.GRAPHQL_URL!, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: op.query }),
    })
    const body = await res.json()
    results.push({
      name: op.name,
      ok: !body.errors,
      errors: body.errors?.map((e: any) => e.message),
    })
  }

  const failures = results.filter(r => !r.ok)
  if (failures.length > 0) {
    console.error('Schema smoke test FAILED:', failures)
    process.exit(1)
  } else {
    console.log('Schema smoke test passed:', results.map(r => r.name).join(', '))
  }
}

smokeTestSchema()
Enter fullscreen mode Exit fullscreen mode

Run this in your CI/CD pipeline before deploying.


Step 5: Add subscription health (if using GraphQL subscriptions)

GraphQL subscriptions use WebSockets. Add a separate health check for the WebSocket endpoint:

// ws-health.ts
import WebSocket from 'ws'

export function checkSubscriptionEndpoint(url: string): Promise<{ status: string; latencyMs?: number; error?: string }> {
  return new Promise((resolve) => {
    const start = Date.now()
    const timeout = setTimeout(() => {
      ws.terminate()
      resolve({ status: 'error', error: 'WebSocket connection timeout' })
    }, 5000)

    const ws = new WebSocket(url, 'graphql-ws')
    ws.on('open', () => {
      clearTimeout(timeout)
      ws.close()
      resolve({ status: 'ok', latencyMs: Date.now() - start })
    })
    ws.on('error', (err) => {
      clearTimeout(timeout)
      resolve({ status: 'error', error: err.message })
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Set up external monitoring

  1. Go to vigilmon.online — free tier.
  2. Create an HTTP(S) monitor for /health (REST endpoint).
  3. Or create a POST monitor targeting your GraphQL endpoint with body assertions.
  4. Interval: 60s
  5. Regions: 2+
  6. Alert on 503 status OR on body containing "errors".

Recap

  1. GraphQL always returns 200 — don't monitor the status code alone.
  2. Add a _healthCheck query to your schema for internal tooling.
  3. Add a /health REST endpoint that returns 200/503 based on dependency health — this is what your uptime monitor hits.
  4. Use body assertions if you must monitor the GraphQL endpoint directly.
  5. Run schema smoke tests in CI to catch breaking changes before deploy.
  6. vigilmon.online supports POST monitors with body assertions for GraphQL-first APIs.

GraphQL is powerful and flexible. That flexibility requires you to be explicit about what "healthy" means. Now you are.

Top comments (0)