Build a status page for your servers

A live uptime page in three files — it fetches your other servers and sites on a timer, marks each up or down with a hard timeout, and shows the result. Deploy it, point it at your services, extend it.

Skip the setup This guide has a one-click starter template that installs everything below onto your server.

If you run more than one thing — a bot's API, a website, a game server's query port — a status page is the single place that tells you, at a glance, what's up and what's down. This one checks each service on a timer with fetch, uses a hard timeout so a dead server can't hang the check, and shows a green or red dot per service. Three files on the Node.js application.

At a glance
You're building A live status/uptime dashboard for your own services
You need A Falix server running the Node.js application
Plan Any — free runs while your session timer has time, premium runs 24/7
Time About thirty minutes

New to Node here? Start with Node.js on Falix.

What it does

Feature How
Check services fetch each URL from sites.json and record up/down
Hard timeout An AbortController fails a hung request after 5 seconds
Live re-check Checks once at boot, then every minute
Status API GET /api/status returns the latest results as JSON
Dashboard public/index.html polls the API and draws the dots

The files

package.json — only Express is needed; fetch is built into Node:

{
  "name": "status-page",
  "version": "1.0.0",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "express": "^4.21.2"
  }
}

sites.json — the things to watch. Edit this to list your own:

[
  { "name": "My website",   "url": "https://example.com" },
  { "name": "My API",       "url": "https://api.github.com" },
  { "name": "My bot's API", "url": "http://123.45.67.89:25570/health" }
]

index.js:

const express = require('express');
const path = require('node:path');
const fs = require('node:fs');

// The list of things to watch lives in sites.json.
const sites = JSON.parse(fs.readFileSync(path.join(__dirname, 'sites.json'), 'utf8'));

// The latest result for each site, updated in place.
const state = sites.map((s) => ({
  name: s.name, url: s.url, status: 'checking', code: null, ms: null, checkedAt: null,
}));

async function checkOne(url) {
  const started = Date.now();
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 5000); // 5s timeout
  try {
    const res = await fetch(url, { signal: controller.signal, redirect: 'manual' });
    const ok = res.status >= 200 && res.status < 400;
    return { status: ok ? 'up' : 'down', code: res.status, ms: Date.now() - started };
  } catch {
    // Timed out, refused, DNS failure — anything that isn't a real response.
    return { status: 'down', code: null, ms: Date.now() - started };
  } finally {
    clearTimeout(timer);
  }
}

async function checkAll() {
  await Promise.all(state.map(async (row) => {
    Object.assign(row, await checkOne(row.url), { checkedAt: new Date().toISOString() });
  }));
}

checkAll();                              // check once at boot
setInterval(checkAll, 60_000).unref();   // then every minute

const app = express();
app.get('/api/status', (req, res) => res.json(state));
app.use(express.static(path.join(__dirname, 'public')));

const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));

public/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Status</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 3rem auto; }
    .site { display: flex; align-items: center; gap: 0.8rem; padding: 0.8rem 0; border-bottom: 1px solid #eee; }
    .dot { width: 0.9rem; height: 0.9rem; border-radius: 50%; background: #999; }
    .up .dot { background: #22c55e; }
    .down .dot { background: #ef4444; }
    .name { flex: 1; font-weight: 600; }
    .meta { color: #666; font-size: 0.85rem; }
  </style>
</head>
<body>
  <h1>Service status</h1>
  <div id="list">Loading...</div>
  <script>
    async function refresh() {
      const sites = await (await fetch('/api/status')).json();
      document.getElementById('list').innerHTML = sites.map((s) =>
        '<div class="site ' + s.status + '"><span class="dot"></span>' +
        '<span class="name">' + s.name + '</span>' +
        '<span class="meta">' + s.status.toUpperCase() +
          (s.ms != null ? ' · ' + s.ms + 'ms' : '') + '</span></div>'
      ).join('');
    }
    refresh();
    setInterval(refresh, 15000);
  </script>
</body>
</html>

How it works

  • The timeout is the whole trick. A plain fetch to a dead server can hang for a long time — long enough to stall every other check. AbortController plus a setTimeout cancels the request after 5 seconds, and the catch turns that into a clean down. Verified against a genuinely unroutable address, the check gives up at exactly 5000 ms.
  • Up vs down is a status range. Any 2xx or 3xx counts as up; anything else — a 5xx, a refused connection, a DNS failure, a timeout — is down. redirect: 'manual' means a redirect is treated as a valid response (the site answered) rather than being followed.
  • Checks run in parallel. Promise.all fires every check at once, so ten services take about as long as the slowest one, not the sum.
  • State lives in memory. The state array holds the latest result and is updated in place; /api/status just serves it. No database needed — a status page only cares about now.

🎯 Good to know: Checking your other Falix servers is exactly what this is for. Use each server's public address and port from its Network page. If a service has a /health route (a great habit — see the healthchecks guide), point at that instead of the front page so you're testing the app, not just the port.

Run it on Falix

  1. Upload the files (create public for index.html), or deploy from Git. Put your real services in sites.json.
  2. Press Start. After npm install, the console prints Listening on port … and the server goes online.
  3. Open your address from the Network page. Within a second the dots turn green or red; the page re-polls every 15 seconds.

The server reads SERVER_PORT and binds 0.0.0.0 already, so nothing to configure for the port. Change sites.json and restart to pick up a new list.

curl http://YOUR_ADDRESS:PORT/api/status
# -> [{"name":"My website","status":"up","code":200,"ms":21, ...}, ...]

Make it yours

  • History and uptime %. Store each check in SQLite (see the URL shortener for the pattern) and compute "up 99.2% over 24h".
  • Alerts. When a service flips to down, post to a Discord channel — the webhook relay and Post to a Discord webhook show the exact call.
  • Check game servers. A TCP/UDP query needs a different probe than HTTP fetch; for now, point at a service's web/health port.
  • Run it on a schedule instead of a timer if you'd rather it live elsewhere — see Automate your server with Schedules.

The check itself is standard fetch and AbortController — the MDN fetch documentation covers options like headers and methods.

Troubleshooting

  • A site you know is up shows down — your server couldn't reach it in 5 seconds. Check the URL is correct and public; private/localhost addresses of other machines aren't reachable from your container. Raise the timeout if a service is genuinely slow.
  • Everything shows "checking" forever — the page can't reach /api/status (the app didn't start) or sites.json is invalid JSON. Read the console; a bad sites.json throws on boot.
  • The page loads but no dotssites.json is an empty array, or a JSON syntax error stopped the parse. Validate the file.
  • App won't start / not reachable — port or bind issue: I can't reach my app.

Next steps

Was this guide helpful?