Hono.js runs on Cloudflare Workers, Bun, Node.js, Deno, and AWS Lambda — often with zero cold starts and global edge distribution. But "runs everywhere" also means "fails in more places." A Cloudflare Worker can time out silently. A Bun process deployed behind a VPS can crash. An edge function can be deployed with a bad environment variable and start returning 500s for hours before anyone notices.
This tutorial covers production-grade uptime monitoring for Hono.js applications using Vigilmon. We will walk through:
- A
/healthroute that works across all Hono.js deployment targets - Vigilmon HTTP monitoring with a 1-minute check interval
- A heartbeat pattern for scheduled Hono.js jobs (Cloudflare Cron Triggers, Bun cron)
- SSL monitoring for custom domains
Prerequisites
- A Hono.js application deployed on Cloudflare Workers, Bun, Node.js, or another supported runtime
- A free account at vigilmon.online
Part 1: Add a health route
Hono's API is runtime-agnostic, so the same health route works whether you deploy on Workers, Bun, or Node.js.
Basic health route
// src/routes/health.ts
import { Hono } from 'hono';
const health = new Hono();
health.get('/', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
runtime: typeof globalThis.Bun !== 'undefined'
? 'bun'
: typeof globalThis.caches !== 'undefined'
? 'cloudflare-workers'
: 'node',
});
});
export default health;
Mount it in your main app:
// src/index.ts
import { Hono } from 'hono';
import health from './routes/health';
const app = new Hono();
app.route('/health', health);
// ... rest of your routes
export default app;
Test it locally:
# Bun
bun run dev
curl http://localhost:3000/health
# Cloudflare Workers (wrangler)
wrangler dev
curl http://localhost:8787/health
Expected response:
{
"status": "ok",
"timestamp": "2026-06-30T10:00:00.000Z",
"runtime": "cloudflare-workers"
}
Health route with dependency checks
For apps that talk to external services (a database, KV store, or third-party API), add dependency checks to the health route:
// src/routes/health.ts
import { Hono } from 'hono';
import type { Bindings } from '../types';
const health = new Hono<{ Bindings: Bindings }>();
health.get('/', async (c) => {
const checks: Record<string, string> = {};
let ok = true;
// Check a Cloudflare KV namespace (skip if not bound)
if (c.env.KV) {
try {
await c.env.KV.put('__health_probe__', '1', { expirationTtl: 60 });
checks.kv = 'ok';
} catch (e) {
checks.kv = `error: ${(e as Error).message}`;
ok = false;
}
}
// Check a D1 database (skip if not bound)
if (c.env.DB) {
try {
await c.env.DB.prepare('SELECT 1').run();
checks.db = 'ok';
} catch (e) {
checks.db = `error: ${(e as Error).message}`;
ok = false;
}
}
return c.json(
{ status: ok ? 'ok' : 'degraded', checks, timestamp: new Date().toISOString() },
ok ? 200 : 503
);
});
export default health;
Vigilmon flags any 503 response as a failure and fires an alert. The 200/503 distinction does the work — you don't need to configure keyword checks in Vigilmon unless you want to be extra explicit.
Skip auth middleware for the health route
If you use Hono middleware for bearer token auth, exclude /health so Vigilmon can reach it without credentials:
// src/middleware/auth.ts
import { createMiddleware } from 'hono/factory';
export const authMiddleware = createMiddleware(async (c, next) => {
// Skip auth for the health endpoint
if (c.req.path === '/health') {
return next();
}
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (!token || token !== c.env.API_SECRET) {
return c.json({ error: 'Unauthorized' }, 401);
}
return next();
});
Apply the middleware globally and the health route stays open:
app.use('*', authMiddleware);
app.route('/health', health); // reachable without a token
Part 2: Set up HTTP monitoring in Vigilmon
Cloudflare Workers
- Log in to vigilmon.online and click Add Monitor.
- Choose HTTP(S) monitor.
- Enter:
https://your-worker.your-subdomain.workers.dev/health(or your custom domain). - Set interval to 1 minute.
- Add your alert channel (email, Slack, or webhook).
- Click Save.
Cloudflare Workers have no server to go down — but they can hit CPU limits, throw unhandled exceptions, or have broken environment bindings. Vigilmon catches all of these via the 503 your health route returns when a dependency check fails.
Bun or Node.js on a VPS
- Use
https://your-domain.com/healthas the monitor URL. - Set interval to 1 minute.
- Set timeout to 10 seconds.
For a Bun server running on a VPS, pair this with a systemd service so the process auto-restarts on crash:
# /etc/systemd/system/hono-app.service
[Unit]
Description=Hono.js App (Bun)
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/opt/hono-app
ExecStart=/usr/local/bin/bun run src/index.ts
Restart=always
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now hono-app
With systemd auto-restart and Vigilmon external monitoring:
- Bun crashes → systemd restarts it within 5 seconds
- If it doesn't come back, Vigilmon detects the outage within 60–120 seconds and alerts you
Part 3: Webhook alerts from Vigilmon
Add a Vigilmon webhook endpoint to your Hono.js app to receive DOWN/UP events and forward them to Slack, Discord, or PagerDuty:
// src/routes/webhooks.ts
import { Hono } from 'hono';
const webhooks = new Hono();
webhooks.post('/vigilmon', async (c) => {
const body = await c.req.json<{
monitor_id: string;
monitor_name: string;
status: 'down' | 'up';
url: string;
checked_at: string;
response_code?: number;
response_time_ms?: number;
}>();
if (body.status === 'down') {
console.error('[VIGILMON] DOWN', {
monitor: body.monitor_name,
url: body.url,
code: body.response_code,
at: body.checked_at,
});
await notifySlack(c.env, body);
} else if (body.status === 'up') {
console.info('[VIGILMON] RECOVERED', { monitor: body.monitor_name });
}
return c.body(null, 204);
});
async function notifySlack(env: { SLACK_WEBHOOK_URL?: string }, body: { monitor_name: string; url: string; response_code?: number }) {
if (!env.SLACK_WEBHOOK_URL) return;
await fetch(env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `*ALERT*: ${body.monitor_name} is DOWN\nURL: ${body.url}\nHTTP: ${body.response_code ?? 'timeout'}`,
}),
});
}
export default webhooks;
Mount it:
app.route('/webhooks', webhooks);
In Vigilmon, set the webhook URL to https://your-app.example.com/webhooks/vigilmon.
Part 4: Heartbeat monitoring for Cloudflare Cron Triggers
Cloudflare Workers support scheduled execution via Cron Triggers. HTTP monitors can not verify these run successfully — only a heartbeat can.
Define the cron trigger
// src/index.ts
import { Hono } from 'hono';
const app = new Hono();
// HTTP routes
app.route('/health', healthRoute);
app.route('/webhooks', webhooksRoute);
// Cloudflare Cron Trigger handler
const scheduled: ExportedHandlerScheduledHandler = async (_event, env, _ctx) => {
try {
await runNightlyCleanup(env);
// Ping Vigilmon heartbeat on success
if (env.VIGILMON_HEARTBEAT_URL) {
await fetch(env.VIGILMON_HEARTBEAT_URL, { method: 'GET' });
}
} catch (err) {
console.error('Scheduled job failed:', err);
// No heartbeat ping on failure — Vigilmon detects the silence
}
};
async function runNightlyCleanup(env: Bindings) {
// your cleanup logic
}
export default {
fetch: app.fetch,
scheduled,
};
Configure the cron in wrangler.toml:
[triggers]
crons = ["0 2 * * *"] # 2am UTC daily
[vars]
# Add VIGILMON_HEARTBEAT_URL via wrangler secret
Set the secret:
wrangler secret put VIGILMON_HEARTBEAT_URL
# Paste: https://vigilmon.online/api/heartbeat/your-unique-token
Create the heartbeat monitor in Vigilmon
- In Vigilmon, click Add Monitor.
- Choose Heartbeat monitor.
- Set expected interval to 25 hours (daily job + 1-hour grace).
- Copy the unique URL and store it as a Worker secret.
If the cron trigger fails to fire (Worker budget exceeded, Cloudflare incident, or a fatal exception), Vigilmon alerts you after 25 hours of silence.
Bun cron (non-Workers)
If you run Hono on Bun with a custom cron implementation:
// src/jobs/cleanup.ts
const HEARTBEAT_URL = process.env.VIGILMON_HEARTBEAT_URL;
const INTERVAL_MS = 60 * 60 * 1000; // 1 hour
async function run() {
try {
await cleanupOldRecords();
if (HEARTBEAT_URL) {
await fetch(HEARTBEAT_URL).catch(() => {
// Non-fatal: don't crash the job if the heartbeat ping fails
});
}
} catch (err) {
console.error('Cleanup job failed:', err);
}
}
// Run immediately, then every hour
run();
setInterval(run, INTERVAL_MS);
Part 5: SSL certificate monitoring
If your Hono.js app runs on a custom domain (not *.workers.dev), add SSL monitoring:
- In Vigilmon, click Add Monitor.
- Choose SSL Certificate monitor.
- Enter your domain:
your-app.example.com. - Set alert threshold: 14 days before expiry.
- Click Save.
Cloudflare manages SSL for Worker custom domains automatically, but manual Let's Encrypt setups on Bun/Node VPSes can silently fail. The 14-day alert gives you time to renew manually.
Summary
Your Hono.js application now has four layers of monitoring:
/healthroute — runtime-agnostic health endpoint; returns503when dependencies are unhealthy, triggering immediate Vigilmon alerts.- HTTP monitor — Vigilmon pings
/healthevery 60 seconds from external regions; any crash, timeout, or degraded state fires an alert. - Heartbeat monitor — your Cloudflare Cron Trigger or Bun interval job pings Vigilmon on each successful run; silence means the job stopped.
- SSL monitor — 14-day early warning before your certificate expires (critical for non-Workers deployments).
Hono's portability means your app can be anywhere. Vigilmon's external monitoring ensures that wherever it is, you know the moment it stops working.
Monitor your Hono.js app free at vigilmon.online
#honojs #cloudflare #bun #javascript #monitoring