Uptime Monitoring for Bun Applications with Vigilmon
Bun's blazing startup speed and drop-in Node.js compatibility make it an appealing runtime for microservices and APIs. But speed at startup doesn't prevent outages at runtime. This tutorial walks you through adding a production-grade health endpoint to a Bun HTTP server and monitoring it with Vigilmon.
What You'll Build
- A Bun HTTP server with a
/healthroute that reflects real app state - A Vigilmon monitor with 5-minute polling from multiple regions
- Webhook alerts wired to Slack (plus email fallback)
Prerequisites
Step 1: Scaffold a Bun HTTP Server
Create a new project:
mkdir bun-monitored-app && cd bun-monitored-app
bun init -i
Replace index.ts with a basic HTTP server:
// index.ts
const PORT = Number(process.env.PORT ?? 3000);
const server = Bun.serve({
port: PORT,
async fetch(req) {
const url = new URL(req.url);
switch (url.pathname) {
case "/":
return new Response("Hello from Bun!", { status: 200 });
case "/health":
return healthHandler();
default:
return new Response("Not Found", { status: 404 });
}
},
});
console.log(`Server running at http://localhost:${server.port}`);
Add the health handler in the same file (or a separate module):
function healthHandler(): Response {
const payload = JSON.stringify({
status: "ok",
runtime: "bun",
version: Bun.version,
timestamp: new Date().toISOString(),
});
return new Response(payload, {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
Start it:
bun run index.ts
Verify:
curl http://localhost:3000/health
# {"status":"ok","runtime":"bun","version":"1.0.20","timestamp":"..."}
Step 2: Add Real Dependency Checks
A health endpoint that always returns 200 isn't actually useful for monitoring. Wire in checks for the things your app actually depends on:
// health.ts
interface CheckResult {
ok: boolean;
latencyMs?: number;
error?: string;
}
async function pingDatabase(): Promise<CheckResult> {
// Replace with your actual DB client ping
// Example shown for a hypothetical SQLite check via bun:sqlite
try {
const start = performance.now();
// const db = new Database("myapp.db");
// db.query("SELECT 1").get();
// Placeholder: always ok for this example
return { ok: true, latencyMs: Math.round(performance.now() - start) };
} catch (err) {
return { ok: false, error: String(err) };
}
}
async function pingCache(): Promise<CheckResult> {
// Replace with your Redis / Bun KV check
try {
const start = performance.now();
// await redisClient.ping();
return { ok: true, latencyMs: Math.round(performance.now() - start) };
} catch (err) {
return { ok: false, error: String(err) };
}
}
export async function buildHealthPayload() {
const [db, cache] = await Promise.all([pingDatabase(), pingCache()]);
const allOk = db.ok && cache.ok;
return {
status: allOk ? "ok" : "degraded",
checks: { database: db, cache },
timestamp: new Date().toISOString(),
httpStatus: allOk ? 200 : 503,
};
}
Update the handler:
import { buildHealthPayload } from "./health";
case "/health": {
const payload = await buildHealthPayload();
const { httpStatus, ...body } = payload;
return new Response(JSON.stringify(body), {
status: httpStatus,
headers: { "Content-Type": "application/json" },
});
}
Vigilmon treats any non-2xx response as a downtime event, so returning 503 when a check fails will trigger an alert automatically.
Step 3: Deploy Your Bun App
Your app needs a public URL before Vigilmon can monitor it.
Fly.io (works great with Bun):
# fly.toml
app = "my-bun-app"
primary_region = "iad"
[http_service]
internal_port = 3000
force_https = true
[[vm]]
memory = "256mb"
cpu_kind = "shared"
cpus = 1
fly launch
fly deploy
Railway / Render also work with zero config — just point them at your repo and set bun run index.ts as the start command.
Note your production URL (e.g. https://my-bun-app.fly.dev).
Step 4: Create a Vigilmon Monitor
- Sign in at vigilmon.online.
- Click + New Monitor.
- Choose HTTP / HTTPS as the monitor type.
- Enter your health URL:
https://my-bun-app.fly.dev/health. - Set Check Interval to 5 minutes.
- Leave Expected HTTP Status as 2xx.
- Click Save Monitor.
Vigilmon starts polling from multiple geographic nodes simultaneously. A downtime event is only recorded when a consensus of regions fails — single-location network hiccups won't wake you at 3 AM.
Step 5: Set Up Webhook Alerts
Slack Integration
- Create a Slack Incoming Webhook for your alerts channel.
- In Vigilmon: Settings → Alert Channels → + Add Channel → Webhook.
- Name it "Slack Alerts", paste the webhook URL, save.
- Open your monitor → Edit → Alert Channels → select "Slack Alerts".
You'll now receive a Slack message like:
🔴 my-bun-app /health is DOWN
Reason: HTTP 503 —{"status":"degraded","checks":{"database":{"ok":false},...}}
Checked at: 2024-01-15 10:31 UTC
And a recovery message when it comes back up:
🟢 my-bun-app /health is back UP (was down 4m 32s)
Email Alerts
Settings → Alert Channels → + Add Channel → Email. Enter your address and assign it to the monitor. Email is a good fallback even if you already have Slack.
Step 6: Validate the Alert Pipeline
Force a failure locally or in staging to confirm alerts fire end-to-end:
// Temporarily break the health endpoint
case "/health":
return new Response(JSON.stringify({ status: "down", reason: "test" }), {
status: 503,
headers: { "Content-Type": "application/json" },
});
Deploy this, wait for the next check cycle (≤ 5 min), and verify the Slack/email notification arrives. Then revert and confirm the recovery alert.
Step 7: Protect the Health Endpoint (Optional)
If you don't want the health response (which may reveal internal dependency states) to be publicly readable, you can add token-based gating:
case "/health": {
const token = req.headers.get("x-health-token");
if (token !== process.env.HEALTH_TOKEN) {
return new Response("Unauthorized", { status: 401 });
}
const payload = await buildHealthPayload();
// ...
}
In Vigilmon, set a custom request header under Monitor → Edit → Request Headers:
| Header | Value |
|---|---|
x-health-token |
your-secret-token |
Bun-Specific Tips
Hot reload in dev: Use bun --hot run index.ts during development so code changes reload without restarting — but always test the health endpoint after a hot reload to confirm handlers rewired correctly.
Bun macros: If you use Bun's compile-time macros, make sure your health handler doesn't accidentally import macro-only modules that won't be available at runtime.
Built-in SQLite: Bun ships bun:sqlite — if your app uses it, a SELECT 1 ping in the health check is zero-overhead and catches "database file locked or corrupted" failures early.
Summary
In under 15 minutes you've added production-grade observability to a Bun application:
-
/healthendpoint with real dependency checks (returns 503 on failure) - Vigilmon polling every 5 minutes from multiple regions
- Slack + email alerts on down/up state changes
Start free at vigilmon.online — no credit card, up to 5 monitors, 5-minute check intervals.
Building something interesting with Bun? Share what you're monitoring in the comments.
Top comments (0)