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
fetchto a dead server can hang for a long time — long enough to stall every other check.AbortControllerplus asetTimeoutcancels the request after 5 seconds, and thecatchturns that into a cleandown. Verified against a genuinely unroutable address, the check gives up at exactly 5000 ms. - Up vs down is a status range. Any
2xxor3xxcounts as up; anything else — a5xx, 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.allfires every check at once, so ten services take about as long as the slowest one, not the sum. - State lives in memory. The
statearray holds the latest result and is updated in place;/api/statusjust 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
/healthroute (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
- Upload the files (create
publicforindex.html), or deploy from Git. Put your real services insites.json. - Press Start. After
npm install, the console printsListening on port …and the server goes online. - 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) orsites.jsonis invalid JSON. Read the console; a badsites.jsonthrows on boot. - The page loads but no dots —
sites.jsonis 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.