API monitoring: what to check and how to set it up

By the PULSX team. Updated . 16 minute read.

TL;DR

Monitor four things on an API: that it answers, that the answer is right, that it is fast enough, and that its certificate and DNS are valid. The simplest sound setup is a /health endpoint that tests your database and other dependencies and returns 200 when all is well and 503 when it is not, requested every 60 seconds from outside your network.

Key takeaways

  • A status code alone can lie: a 200 with an error in the body looks healthy to a simple monitor.
  • Build a deep /health endpoint that tests each dependency with a 1 second limit and returns 503 when one fails.
  • A 401 from a protected endpoint checked without credentials is a healthy answer. A 429 never is.
  • Give the monitor its own read-only key, send it in a header, and rotate it on a schedule.
  • Use percentiles for latency. An average of 204 ms can hide two requests in every hundred that take 5 seconds.
  • With a 5 minute interval, only about 30% of 3 minute outages raise an alert. Check every 60 seconds.

How is monitoring an API different from checking a web page?

API monitoring checks that a program can call your API and get a correct answer in good time. It differs from checking a web page because nobody is looking at the result. The caller is code, and code cannot tell you that something "looks wrong".

Web pageAPI
Who calls itA person in a browserAnother program: your app, a mobile client, a partner's server
How a failure showsThe person sees an error and may tell youThe caller retries, fails quietly or shows its own vague error
What "correct" meansThe page rendersThe status code and the data are both right
A 200 responseNearly always means it workedCan still carry an error in the body

Three things follow from this.

  • Status codes carry the meaning. Your API has to return honest ones.
  • You need a URL built for checking. There is no home page to load. You write a small endpoint, usually /health, that answers one question: can this API do its job right now?
  • The monitor has to deal with authentication. It either carries a key of its own, or it checks an endpoint where "not authorised" is the expected answer.

What should you monitor on an API?

Watch four things: availability, correctness, latency, and the certificate and DNS that clients need before they can reach you at all.

WhatThe questionThe simplest check
AvailabilityDoes it answer?An HTTP request every minute that expects a status from 200 to 299
CorrectnessIs the answer right?A /health endpoint that tests its dependencies and returns 503 when one fails
LatencyIs it fast enough?The response time of each check, compared with a limit
Certificate and DNSCan clients find it and trust it?A certificate expiry check and a DNS record check

Why the fourth one is on the list

An expired certificate takes an API down while every server is healthy. A browser shows a warning a person can click past. An API client refuses the connection, and every call fails.

DNS is the same kind of failure. If the name stops resolving, or points at an old address, no request arrives and your own logs show nothing wrong.

You can check both by hand now with the SSL checker and the DNS lookup tool.

What a plain uptime monitor covers

A PULSX HTTP monitor covers availability and records the response time of every check. Certificate expiry is tracked on every HTTPS monitor, and DNS records have their own monitor type. It does not read the response body, so correctness has to come from the status code. The /health endpoint later in this guide is how you do that.

Which status codes mean an API is healthy?

Healthy is the code the endpoint should return when it works. For most checks that is anything from 200 to 299. For a protected endpoint checked without credentials, it is 401.

CodeWhat it means to a monitorHealthy?
200, 204The request workedYes, as long as the body is right too
401No credentials, or wrong onesYes, if the monitor sends none on purpose. No, if it sends a key.
403Credentials understood, action not allowedNo. The key lost a permission, or a firewall is blocking the monitor.
404No such routeNo. A deploy removed or renamed it.
429Too many requestsNo. The monitor is hitting your rate limit.
500, 502, 503, 504The code hit an error it did not handle, or a proxy could not get an answer from the app in timeNo

The guide to HTTP status codes explains each of these in full. The definitions are in RFC 9110.

Why a 401 can be good news

To return 401, the name must resolve, the certificate must be valid, the server must be running and the code that checks credentials must have answered. It needs no secret in your monitoring tool.

In PULSX you set the healthy codes as a list or as ranges (the default is 200 to 299), so one check can expect exactly 401. If that endpoint ever returns 200 without credentials, the check fails. That is correct: an endpoint that should be closed is open.

Why a 200 can be bad news

Some APIs return 200 for everything and put the failure in the body:

HTTP/1.1 200 OK
Content-Type: application/json

{"error": "database unavailable"}

GraphQL APIs commonly do this by design, with an errors field in a 200 response. A monitor that reads only the status code sees a healthy API.

There are two fixes. Use a tool that reads the response body, or give the monitor an endpoint whose status code tells the truth.

How do you design a /health endpoint properly?

Build two endpoints. A shallow one says the process is running. A deep one tests each dependency and returns 200 when all pass and 503 when any fails. Point your uptime monitor at the deep one.

Shallow checkDeep check
Typical path/health/live/health
What it testsThe process can answer HTTPThe things a real request needs
Who calls itA load balancer or container orchestratorYour uptime monitor

Keep them apart. If an orchestrator restarts your app whenever the deep check fails, a database outage becomes a database outage plus a restart loop.

What the deep check should test

  • Database: run SELECT 1 through the same connection pool the app uses.
  • Cache: send a PING.
  • Queue: reach the broker. Better still, fail when the oldest waiting job is more than, say, 10 minutes old.
  • A critical third party: only one your API cannot work without. Cache the result for 30 to 60 seconds, so one check a minute does not become one paid call a minute.

Rules for the endpoint

  • Return 503 when a dependency is down. The status code carries the answer. The body is detail for people.
  • Keep it fast. Give each dependency a limit of 1 second and run them at the same time. A dependency that is too slow counts as failed.
  • Keep it cheap. No table scans, no writes. One check a minute is 1,440 calls a day.
  • Do not leak internals. No hostnames, versions, connection strings or error messages. Return "ok" or "fail" for each check and write the real error to your logs.
  • Do not cache the response. Send Cache-Control: no-store, or a CDN may serve an old 200.

Worked example: Node (Express)

const express = require('express');
const { Pool } = require('pg');
const { createClient } = require('redis');

const app = express();
const db = new Pool({ connectionString: process.env.DATABASE_URL });
const cache = createClient({ url: process.env.REDIS_URL });
cache.on('error', () => {});
cache.connect().catch(() => {});

// Run one check with a 1 second limit. Never throws.
async function check(fn) {
  const limit = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('timeout')), 1000)
  );
  try {
    await Promise.race([fn(), limit]);
    return 'ok';
  } catch {
    return 'fail';
  }
}

// Shallow: the process is running.
app.get('/health/live', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Deep: the things a real request needs are reachable.
app.get('/health', async (req, res) => {
  const [database, cacheState] = await Promise.all([
    check(() => db.query('SELECT 1')),
    check(() => cache.ping()),
  ]);
  const healthy = database === 'ok' && cacheState === 'ok';
  res.set('Cache-Control', 'no-store');
  res.status(healthy ? 200 : 503).json({
    status: healthy ? 'ok' : 'fail',
    checks: { database, cache: cacheState },
  });
});

app.listen(3000);

Worked example: Python (FastAPI)

import asyncio
import os
from contextlib import asynccontextmanager

import asyncpg
import redis.asyncio as redis
from fastapi import FastAPI, Response


@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.db = await asyncpg.create_pool(os.environ["DATABASE_URL"])
    app.state.cache = redis.from_url(os.environ["REDIS_URL"])
    yield
    await app.state.db.close()
    await app.state.cache.aclose()


app = FastAPI(lifespan=lifespan)


async def check(call) -> str:
    """Run one check with a 1 second limit. Never raises."""
    try:
        await asyncio.wait_for(call(), timeout=1.0)
        return "ok"
    except Exception:
        return "fail"


# Shallow: the process is running.
@app.api_route("/health/live", methods=["GET", "HEAD"], include_in_schema=False)
async def live():
    return {"status": "ok"}


# Deep: the things a real request needs are reachable.
@app.api_route("/health", methods=["GET", "HEAD"], include_in_schema=False)
async def health(response: Response):
    database, cache = await asyncio.gather(
        check(lambda: app.state.db.execute("SELECT 1")),
        check(lambda: app.state.cache.ping()),
    )
    healthy = database == "ok" and cache == "ok"
    response.status_code = 200 if healthy else 503
    response.headers["Cache-Control"] = "no-store"
    return {
        "status": "ok" if healthy else "fail",
        "checks": {"database": database, "cache": cache},
    }

Both return the same thing: 200 with every check "ok", or 503 with the failed check marked "fail". Add one check line for each extra dependency.

How do you monitor an endpoint that needs authentication?

Create one API key that exists only for monitoring, give it read-only access to as little as possible, and have the monitor send it in a request header. Never use a person's key or your production admin key.

Setting up the key

  1. Make a separate account or client named for its purpose, for example uptime-monitor. It should own no real customer data.
  2. Give it read-only scope on the one or two endpoints you check.
  3. Send it in a header, for example Authorization: Bearer YOUR_MONITOR_KEY. Do not put it in the URL as a query parameter, because URLs are written to access logs.

Rotating it

Rotate the key on a schedule (every 90 days is a common choice) and whenever someone with access to it leaves. Do it in this order so the check never fails:

  1. Create the new key. Both keys now work.
  2. Put the new key in the monitor's header and wait for two or three passing checks.
  3. Revoke the old key.

When a header is not enough

Some APIs need a step before the real call, such as posting credentials to get a short-lived token. A monitor that sends one request cannot do that.

PULSX HTTP monitors can send custom request headers, including Authorization. They do not send a request body and do not run multi-step or scripted checks. If your API needs a token exchange, either use a tool built for scripted checks, or let your deep /health endpoint do the work on the server and report 200 or 503.

Should the check use HEAD or GET?

Use GET for a health endpoint. Use HEAD when the response is large and you only need the status code. HEAD asks for the same response as GET with the body left out.

HEADGET
What comes backStatus line and headers onlyStatus line, headers and body
Data sent over the networkA few hundred bytesThe whole response
RiskSome frameworks and proxies do not answer HEAD on a route written for GET, and return 405None, apart from size
Best forLarge pages, file downloads, big list endpointsHealth endpoints and anything small

A health response is tiny. The body in the examples above is under 100 bytes, so HEAD saves nothing. GET is also the method your real callers send.

If you do choose HEAD, test it by hand first with curl -sS -I. A 405 means the route does not answer HEAD. Section 9.3.2 of RFC 9110 says a server should send the same headers for HEAD as for GET, but not every framework wires that up for you. The FastAPI example above lists HEAD in methods for that reason.

How should you measure latency?

Measure latency with percentiles, not averages. A percentile tells you how long the slowest share of requests took, and the slowest share is where trouble shows first.

The 95th percentile, written p95, is the time that 95 of every 100 requests finished within. To find it, sort the times from fastest to slowest and take the 95th. For p99, take the 99th.

A worked example

Take 100 requests. 90 of them took 80 ms, 8 took 400 ms and 2 took 5,000 ms.

MeasureValueWhat it tells you
Average204 msLooks fine. It is a time that no request actually took.
p50 (the median)80 msWhat a typical request gets
p95400 ms1 request in 20 is this slow or slower
p995,000 ms1 request in 100 waits 5 seconds or more

The average is (90 x 80 + 8 x 400 + 2 x 5,000) divided by 100, which is 204 ms.

The 5 second requests matter more than 2% suggests. If one screen of your app makes 10 API calls, the chance that at least one is a 5 second call is about 18%.

The time an outside check records includes the DNS lookup, the connection and the TLS handshake, and it is measured from the monitor's location. Treat it as a steady reference line, not as what every user sees.

Picking a threshold

  1. Collect a week of response times. At one check a minute that is 10,080 samples.
  2. Find the p99. Say it is 320 ms.
  3. Set "too slow" at about three times that, rounded: 1,000 ms. This is a rule of thumb to start from.
  4. Keep it below your callers' own timeout. If clients give up after 5 seconds, a 10 second threshold tells you nothing.
  5. Alert only when it is breached several checks in a row.

If your monitor alerts on status codes only, put the threshold inside the health endpoint. Both examples above already do this: a dependency that takes longer than 1 second counts as failed, and the endpoint returns 503.

How often should you check, and how do you avoid your own rate limit?

Check every 60 seconds for anything customers depend on. The interval sets how late you find out about an outage, and how short an outage can be before you never see it.

The numbers below assume a failed check is retried 3 times, 30 seconds apart, before an alert goes out.

Every 1 minuteEvery 5 minutes
Checks per day1,440288
Alert after the outage starts, at best1 min 30 s1 min 30 s
Alert after the outage starts, on average2 min4 min
Alert after the outage starts, at worst2 min 30 s6 min 30 s
A 3 minute outage is seen by a checkAlways60% of the time
A 3 minute outage raises an alertAlways30% of the time

The last two rows work like this. A 3 minute outage lasts 180 seconds. The chance that a check every 300 seconds lands inside it is 180 in 300, which is 60%.

To raise an alert, the first failed check and all three retries must fall inside the outage, so the first failure has to land in its first 90 seconds. That is 90 in 300, which is 30%.

PULSX checks as often as every 60 seconds, and 3 retries 30 seconds apart is its default.

Not tripping your own rate limit

A monitor is a client like any other, and your rate limiter will treat it as one. When it is limited, it receives 429 (Too Many Requests, defined in RFC 6585) and you get an outage alert for an API that is working.

One monitor at one check a minute sends 1,440 requests a day, which is small. The trouble starts when the monitor shares a key, and so an allowance, with a real integration.

The fixes: leave /health out of the rate limiter and keep it cheap, give the monitor key its own limit, and never add 429 to the list of healthy codes. A 429 on a check means the setup is wrong, and hiding it hides real outages too.

Synthetic checks, real user metrics or logs: which do you need?

Start with synthetic checks, add logs and server metrics next, and add real user metrics last.

  • A synthetic check is a request a monitor sends on a timer from outside your network. Nobody real is waiting for the answer.
  • Real user metrics are timings and error counts recorded for the requests your real callers make, measured in the client or at the edge of your network.
  • Logs and server metrics are what your own servers record about each request and about themselves.
Synthetic checksReal user metricsLogs and server metrics
AnswersIs it up right now?What are callers actually getting?Why did it fail?
Works at 03:00 with no trafficYesNo. No requests, no data.Partly. Errors need requests to appear.
Sees DNS and certificate failuresYesOften not. The request never completes, so nothing is reported.No. The request never arrives.
Explains the causeNoNoYes

A synthetic check is the only one of the three that reports an outage when nobody is using the API, and it takes minutes to set up. Logs come second, because the question after "it is down" is "why". Real user metrics find the slow endpoint in one country that no synthetic check was pointed at.

How do you monitor third-party APIs you depend on?

Watch them from two sides: an outside check on the provider's own endpoint, and a count of failed calls inside your code. The first tells you they are down. The second tells you whether it is hurting you.

Start with a list of the services your API cannot work without. Payments, email sending, sign-in and storage are the usual ones.

Checking from outside

  • Use an endpoint that needs no key if the provider has one, such as a public status or ping URL.
  • Or expect a 401. Many APIs answer 401 to a request with no credentials. Check what theirs returns, then set that as the healthy code. It proves their API is reachable without you storing their key in a monitor.
  • Check every 5 minutes, not every minute. It is their service and their rate limit. Read their terms first.
  • Do not put a live production key for a payment or email provider into a monitor. If you must authenticate, use a restricted or test key.

For a quick look by hand, the live status page for well-known services shows whether a provider is up right now.

Checking from inside

The outside check does not prove your own calls work. Your key may have expired, or your account may be over its quota. So log every call to the provider with its status and duration, and alert when failures pass a level you choose, for example 5 in 100 calls over 5 minutes.

Which check catches which failure?

No single check catches everything. Use this table to see what each kind of failure needs, and where a status-only monitor stops.

FailureWhat detects it
Timeout (the request hangs)Any HTTP check. No answer within the check's time limit counts as a failure.
5xx errorAn HTTP check with healthy codes set to 200 to 299
Wrong body (a 200 with an error or bad data inside)A tool that reads the response body, or a /health endpoint that runs the test itself and returns 503
Slow (correct answers that arrive late)A response time threshold, or a time limit inside /health that turns "slow" into 503. Real user metrics for the full picture.
Expired certificateA certificate expiry check that warns days ahead. After expiry, the HTTPS check itself fails.
DNS failure ("could not resolve host")A DNS record check. The HTTP check also fails, because it cannot find the server.
Failure in one region onlyChecks from several locations, or real user metrics

Where PULSX stops

PULSX catches the timeout, 5xx, certificate and DNS rows directly. It does not read response bodies, has no keyword or JSON checks and no multi-step checks, and it checks from one location.

So the "wrong body" and "slow" rows need the health endpoint approach: put the test on your server and let the status code carry the answer. The "one region only" row needs a different tool or real user metrics.

What is the set-up checklist, with commands to test each check?

Run each command by hand before you create the monitor. Replace api.example.com with your own host.

1. The health endpoint answers 200

curl -sS -o /dev/null -w '%{http_code}\n' https://api.example.com/health

Expect 200.

2. It answers 503 when a dependency is down

In staging, stop the database and run the same command against the staging host. Expect 503. If you get 200, the check is shallow.

3. A protected endpoint refuses a request with no key

curl -sS -o /dev/null -w '%{http_code}\n' https://api.example.com/v1/orders

Expect 401.

4. The monitor key works, and only for reading

curl -sS -o /dev/null -w '%{http_code}\n' \
  -H 'Authorization: Bearer YOUR_MONITOR_KEY' \
  'https://api.example.com/v1/orders?limit=1'

Expect 200. Then, in staging only, try a write with the same key and expect 403.

5. Response time is where you think it is

curl -sS -o /dev/null \
  -w 'dns %{time_namelookup}  connect %{time_connect}  tls %{time_appconnect}  first byte %{time_starttransfer}  total %{time_total}\n' \
  https://api.example.com/health

Each value is in seconds, counted from the start of the request. A large gap between tls and first byte is time spent in your server.

6. The certificate is valid and not close to expiry

curl -sS -vI https://api.example.com/health 2>&1 | grep -i 'expire date'

If your build of curl prints nothing here, use openssl:

openssl s_client -connect api.example.com:443 -servername api.example.com \
  </dev/null 2>/dev/null | openssl x509 -noout -enddate

7. DNS returns the address you expect

dig +short api.example.com

8. Create the monitors

  • One HTTP monitor on /health: GET, every 60 seconds, healthy codes 200 to 299.
  • One HTTP monitor on a protected endpoint, with either no key and healthy code 401, or the monitor key in a header and healthy codes 200 to 299.
  • One DNS monitor on the API's hostname.

9. Test the alert

Break the staging health check on purpose and wait for the alert to arrive. Then make sure it goes to a named person. The on-call guide covers who that should be and what happens when they do not answer.

All of step 8 fits in a free PULSX account: 20 monitors, checks every 60 seconds, and certificate expiry tracked on every HTTPS monitor. Sign up, no card needed.

Common questions

What is API monitoring?+
API monitoring is sending test requests to an API on a timer and alerting a person when the answer is missing, wrong or too slow. It covers four things: availability, correctness, latency, and the certificate and DNS records that clients need before they can connect at all.
What should a health check endpoint return?+
It should return status 200 when the API and its dependencies are working, and 503 when any required dependency is down or too slow. The body can list each check as ok or fail, but it should not contain hostnames, versions or error messages. Send Cache-Control: no-store so the answer is never cached.
What is the difference between a shallow and a deep health check?+
A shallow check only proves the process can answer an HTTP request. A deep check also tests the things a real request needs, such as the database, the cache and the queue. Use the shallow one for load balancers and orchestrators, and point your uptime monitor at the deep one.
Is a 401 response a sign that the API is down?+
No. A 401 from a protected endpoint that was called without credentials means the API is reachable and its authentication is working. You can use it as a healthy result for a check that sends no key. A 401 is a failure only when the monitor did send a key, because then the key has expired or been revoked.
Should an API health check use HEAD or GET?+
Use GET for a health endpoint. Its response is tiny, so HEAD saves nothing, and GET is the method your real callers use. HEAD is useful for large responses where you only need the status code, but test it first, because some frameworks return 405 for HEAD on a route written for GET.
How often should you check an API?+
Every 60 seconds for anything customers depend on. With 3 retries 30 seconds apart, a 1 minute interval alerts you between 1.5 and 2.5 minutes after an outage starts. A 5 minute interval can take 6.5 minutes, and raises an alert for only about 30% of outages that last 3 minutes.
What do p95 and p99 latency mean?+
p95 is the response time that 95 of every 100 requests finished within, and p99 is the same for 99 of every 100. They show how slow the slowest requests are. An average hides this: 100 requests can average 204 ms while two of them take 5 seconds.
Can an uptime monitor check the response body of an API?+
Some tools can and some cannot. PULSX does not read the response body: it checks the status code and records the response time. The workaround that works with any tool is a /health endpoint that runs the test on your server and returns 503 when the result is wrong.

More guides

Free DNS, SSL and cron tools

Know the minute your site goes down.

PULSX checks your site, API, cron jobs and SSL certificate every 60 seconds. Free for 20 monitors, with no card.