tutorial

Uptime monitoring for Hono.js applications

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...

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 /health route 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

  1. Log in to vigilmon.online and click Add Monitor.
  2. Choose HTTP(S) monitor.
  3. Enter: https://your-worker.your-subdomain.workers.dev/health (or your custom domain).
  4. Set interval to 1 minute.
  5. Add your alert channel (email, Slack, or webhook).
  6. 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

  1. Use https://your-domain.com/health as the monitor URL.
  2. Set interval to 1 minute.
  3. 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:

  1. Bun crashes → systemd restarts it within 5 seconds
  2. 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

  1. In Vigilmon, click Add Monitor.
  2. Choose Heartbeat monitor.
  3. Set expected interval to 25 hours (daily job + 1-hour grace).
  4. 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:

  1. In Vigilmon, click Add Monitor.
  2. Choose SSL Certificate monitor.
  3. Enter your domain: your-app.example.com.
  4. Set alert threshold: 14 days before expiry.
  5. 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:

  1. /health route — runtime-agnostic health endpoint; returns 503 when dependencies are unhealthy, triggering immediate Vigilmon alerts.
  2. HTTP monitor — Vigilmon pings /health every 60 seconds from external regions; any crash, timeout, or degraded state fires an alert.
  3. Heartbeat monitor — your Cloudflare Cron Trigger or Bun interval job pings Vigilmon on each successful run; silence means the job stopped.
  4. 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

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →