The health screen and what each probe means

The eleven probes behind the Platform Health card, what each one actually measures, how the 0-100 score is arrived at, and what to check first for every red service.

9 min readUpdated 12 August 2026health, monitoring, probes, scheduler, redis

"Platform health: degraded" is not an answer on its own. This page says what was measured, which threshold tripped, and which of the eleven probes is worth acting on.

Where it lives

There is no standalone health screen. The surface is the Platform Health card on the admin dashboard, /admin, which polls GET /api/admin/system/health/batch every five minutes and draws:

  • a score ring, 0-100 — green at 80 and above, amber at 60-79, red below 60;
  • the overall status word — healthy, warning or critical;
  • counts of services up, in warning and down;
  • a Scheduler line of its own, above the service list, fed by a separate call to GET /api/admin/system/cron/scheduler;
  • Show N services, which expands every probe with its status, its latency in milliseconds where it has one, a Critical tag where the probe is weighted as critical, and its message — the message is printed only for services in warning or down.

A failed refresh leaves the previous figures on screen rather than blanking the card, so a frozen score can mean the API is unreachable rather than that nothing changed. Use the refresh button in the card header to re-run the checks now.

Both the batch endpoint and its single-service sibling require authentication and the catch-all access.admin. The scheduler line needs view.cron as well, and is dropped — rather than taking the whole card down — for an operator who does not hold it.

Two endpoints, and they are not the same thing

Runs every probe and returns a score

Returns overall (score and status), a services array of {name, status, message, latency, critical}, and a timestamp. Service status is one of up, warning, down or unconfigured. This is what the dashboard card draws.

If the whole endpoint throws, it does not error — it returns a score of 50, status warning, and a single service called Health Check carrying the exception message. A card showing exactly one service is that fallback.

Probes ONE integration by name

A different, older endpoint driven by ?service=. Nothing in the admin panel calls it; it is a manual tool. Valid names are email, stripe, transfi, sms, openexchangerates, googletranslate, ethereum, bsc, polygon, ftm, optimism, arbitrum, celo, and scylla — the last only when the ecosystem extension is enabled. An unrecognised name returns an empty object rather than an error.

The first successful check for a given service is cached in memory and returned for every later call, with no expiry. So after you fix a credential, this endpoint keeps reporting the old result until the backend restarts. The batch endpoint has no such cache and re-runs everything on each call.

?service=email is also not a passive check: it enqueues a real message to NEXT_PUBLIC_APP_EMAIL.

The eleven probes

Probe What it actually tests Weighted critical
Database COUNT over the users table, timed yes
Cache (Redis) PING, with a 3-second ceiling yes
Scheduler (Cron) The scheduler heartbeat in Redis, against this process's CRON_MODE yes
Email Service Whether the configured transport's variables are present no
Exchange Provider An active provider row, plus its API key and secret in the environment no
Transaction Queue Pending transactions, and how old the oldest are no
Withdrawal Queue Pending withdrawals past the withdrawal SLA no
KYC Queue Pending applications past seven days, and total backlog no
Support Queue Open tickets, and how many are high priority no
Error Rate Failed transactions as a share of the last 24 hours on the >10% branch
Blockchain RPC A raw JSON-RPC block-number call to a configured endpoint no

Blockchain RPC is omitted entirely unless the ecosystem or wallet_connect extension is enabled, so most installs see ten services rather than eleven. That is why the card says "Show 10 services" on a plain trading install and there is nothing missing.

Database

Times a single COUNT against the users table.

  • warning above 1000 ms — connected but slow. The latency is in the message and in the card's own latency column.
  • down — the query threw; the message carries the driver's error.

Critical, so a down here forces the overall status to critical and takes 40 points off the score on its own.

Cache (Redis)

Sends PING and expects PONG, giving up after 3 seconds.

  • warning above 500 ms, or on an unexpected reply.
  • down when the ping fails, with the message "Scheduled jobs and cross-process cache invalidation are stopped until it returns."

Also critical. Redis is a hard dependency, not a cache — sessions, rate limits, distributed locks, the job queue and cross-process settings invalidation all live in it, and the in-memory fallback was removed. A down here on a running backend means Redis went away after boot, because an unreachable Redis at boot stops the process with exit 78 instead. See Troubleshooting.

Scheduler (Cron)

The highest-consequence, lowest-visibility failure in the product, which is why it gets its own line rather than a row eight deep in a collapsed list. When the cron app dies every other probe stays green — the database answers, Redis answers, the API answers — while nothing scheduled runs.

It is scored from three inputs: readSchedulerHeartbeat(), the staleness threshold SCHEDULER_STALE_MS (60 seconds), and cronMode() — this process's own CRON_MODE.

What it finds Status Meaning
A heartbeat naming a peer, fresher than 60s down Two processes are registering jobs. Every scheduled job can run twice over the same rows, withdrawals included
This process is inline or only up The scheduler is here; the message names the mode and the registered job count
CRON_MODE=off and no heartbeat at all, within 240s of this process starting warning Still booting — the cron app registers ~60 workers and reports in later than the web tier
CRON_MODE=off and no heartbeat after that grace period down No scheduler is running anywhere
A heartbeat older than 60s down The scheduler stopped reporting; the message says how long ago and on which instance
The heartbeat could not be read warning Redis is the transport, not the subject — the Cache probe above already reports it

Nothing on this card knows whether an individual job succeeded. This probe answers one question — is any process registering jobs, and exactly one? A scheduler that is alive while a single job refuses itself every tick reports up here.

Start with pm2 list and pm2 logs cron. Per-job state lives on System → System Monitoring → Scheduled Tasks (/admin/system/cron); see The scheduler console.

Email Service

A configuration check, not a delivery check. It reads APP_EMAILER — defaulting to nodemailer-service when unset — and asks whether that transport's variables are present:

APP_EMAILER Considered configured when
local always — it shells out to sendmail
nodemailer-smtp APP_NODEMAILER_SMTP_HOST and APP_NODEMAILER_SMTP_SENDER are set
nodemailer-service APP_NODEMAILER_SERVICE, _SENDER and _PASSWORD are all set
nodemailer-sendgrid APP_SENDGRID_API_KEY is set
anything else the variable is non-empty

A missing configuration reports unconfigured, which costs the score nothing — the platform does not assume you want mail. A failure of the check itself reports warning. Neither state means a send failed, and this probe will happily read up while every message bounces.

To test delivery you need the notification console, not this card. See Notification delivery operations.

Exchange Provider

Looks for an exchange row with status: true, then for APP_<PROVIDER>_API_KEY and APP_<PROVIDER>_API_SECRET in the environment.

  • No active provider → unconfigured, no score penalty.
  • Active but credentials missing → warning, naming the provider.
  • Both present → up.

It is a presence check. It does not call the exchange, so a revoked key, an IP ban or a rate limit all show up. For the real thing, see The exchange provider is degraded.

The four queue probes

These measure backlog age against the SLA cutoff in backend/src/utils/sla.ts — the same numbers the header inbox uses. (The KYC probe writes its seven days out as a literal rather than reading the file, but it is the same seven days.) A warning means old unactioned rows, not a broken queue and not a stopped worker.

Probe Warns when
Transaction Queue any transaction has been PENDING for more than 72h, or more than 500 are pending
Withdrawal Queue any WITHDRAW has been PENDING for more than 168h (7 days)
KYC Queue any application has been PENDING for more than 7 days, or more than 50 are pending
Support Queue any OPEN ticket carries importance: HIGH

The message always names the count, so "3 withdrawals pending >7 days" is telling you to open the withdrawal queue, not to restart anything.

The header inbox counts a ticket as waiting on you at PENDING or OPEN. This probe counts OPEN alone, and it warns on high-priority tickets rather than on age. The two numbers are measuring different things and are expected to differ; the inbox is the one to work from.

Three of the four queue probes are written to fail soft: if the model is missing or the query throws, Transaction Queue, Withdrawal Queue and Support Queue all report up rather than manufacturing an outage — "Transaction system ready", "Withdrawal system ready", "Support system ready", and "No open tickets" where the support model itself is absent. So those three never being red is not proof the queue is healthy.

KYC Queue is the exception. Only its missing-model branch reports up ("KYC system ready"); a query that throws reports warning with the message "Could not check KYC queue", which costs the score 5 points. That amber means the probe could not run, not that applications are piling up — read the message before you open the KYC screen.

Error Rate

Failed transactions as a percentage of all transactions created in the last 24 hours.

  • above 10%warning, and flagged critical, which pulls 15 points off rather than 5.
  • above 5%warning.
  • No transactions in the window → up, "No transactions in last 24h".

A spike here is usually one payment rail failing. Cross-check the Deposits queue and A customer paid and the balance did not move.

Blockchain RPC

Only present when ecosystem or wallet_connect is enabled. It walks the networks ETH, BSC, POLYGON, FTM, ARBITRUM, OPTIMISM, takes the first that has a <NETWORK>_MAINNET_RPC or <NETWORK>_TESTNET_RPC set, and issues a raw eth_blockNumber call with a 5-second timeout — probeEvmRpcBlockNumber, a plain fetch rather than an ethers provider, because a provider retries network detection every second forever when the node is down.

  • No endpoint configured at all → unconfigured.
  • First endpoint that answers → up, naming that network.
  • Endpoints configured, none answered → warning, "RPC configured but connection failed".

This is a reachability check for one endpoint, not a survey. ETH is tried first, so a healthy Ethereum RPC reports up while every other chain is down.

Polygon in particular can never be the network it names: the probe looks for POLYGON_MAINNET_RPC / POLYGON_TESTNET_RPC, while Polygon's endpoints are configured as POLYGON_MATIC_RPC (and POLYGON_MATIC-MUMBAI_RPC). Use GET /api/admin/system/health?service=polygon, which reads the right variables, to check that chain.

A red Blockchain RPC is an .env or upstream-node problem. It is not a platform fault and no restart fixes it.

How the score is arrived at

Start at 100 and deduct:

Finding Deduction
A critical service down 40, and the overall status is forced to critical
A non-critical service down 15
A critical service in warning 15
A non-critical service in warning 5
unconfigured nothing

Then: critical if any critical service is down or the score is under 50; warning under 80; otherwise healthy.

Two consequences worth internalising. A single critical outage — Database, Redis or the scheduler — takes the card to critical on its own, however green everything else is. And a pile of queue warnings can push you to warning without anything being broken: five non-critical warnings is 25 points, and 75 is below the healthy line. Read the service list before you treat an amber card as an incident.

What to check first for each red service

Service First move
Database down pm2 logs backend --lines 200; then MySQL itself. The API is dead too, so the site is down — see Troubleshooting
Database warning Slow query or an overloaded box. Check load and the MySQL slow log — see Logs
Cache (Redis) redis-cli -h $REDIS_HOST -p $REDIS_PORT ping. Check REDIS_PASSWORD; an auth failure looks identical to a refused connection
Scheduler (Cron) down pm2 list, then pm2 logs cron. If the message names two instances, stop whatever was started outside pnpm start
Scheduler (Cron) warning Usually the boot grace period or an unreadable Redis. Wait one poll, then read the Cache row
Email Service unconfigured APP_EMAILER and its transport block in .env. Values are in Environment variables
Exchange Provider /admin/finance/exchange — verify credentials there, then The exchange provider is degraded
Transaction / Withdrawal Queue Work the queue: Queue triage, A withdrawal is stuck
KYC Queue /admin/crm/kyc/application — see KYC
Support Queue /admin/crm/support, filtered to Waiting
Error Rate /admin/finance/transaction filtered to FAILED over the last day, then the gateway those rows belong to
Blockchain RPC The *_RPC variables in .env, and your node provider's status page. Restart the backend after changing them

/api/health is the probe; this card is the diagnosis

This card is an admin surface: it needs a session and the access.admin permission, and it runs eleven checks. That makes it the wrong thing for a load balancer to poll and the right thing for a human to read once something pages.

The probe is GET /api/health — unauthenticated, two checks, cheap enough to poll every few seconds forever. It answers 200 while the backend can serve and 503 when it cannot, so a monitor can act on the status line alone:

{"status":"ok","uptime":8241,"timestamp":"2026-08-12T12:25:47.738Z",
 "checks":{"database":{"status":"up","latency":1},"redis":{"status":"up","latency":1}}}

status is ok or degraded, and degraded still answers 200 — Redis being gone stops scheduled jobs and cache invalidation but does not stop this process serving requests. The database is the only check that turns into a 503. See Monitoring for what to alert on.

The maintenance server answers the same URL, and answers 503

While the platform is stopped, pnpm stop leaves a lightweight maintenance server holding port 3000 and the backend port. It answers /health and /api/health with HTTP 503 and the body:

{"status":"maintenance","message":"Site is under maintenance"}

Everything else under /api/ gets a 503 JSON body with Retry-After: 300, and every other path gets the 503 HTML maintenance page.

The maintenance server binds the backend port precisely so callers get a 503 instead of a refused connection, which means your uptime monitor sees a 503 on /api/health during every planned update as well as during a real outage.

{"status":"maintenance"} is the maintenance server and is not an incident. {"message":"Database unreachable: ...","statusCode":503} is the real backend telling you MySQL has gone, and is. Port 4001 is deliberately left unbound during maintenance — never point a monitor at it.