Setting Up Uptime Monitoring for Remix Apps with Vigilmon
Remix is a full-stack React framework built around web standards — loader functions, actions, and nested routes make it a joy to build with. But when a Remix app goes down, you want to know immediately, not when a user tweets at you.
This tutorial shows you how to add a health-check route to a Remix app and connect it to Vigilmon for multi-region uptime monitoring with email and webhook alerts.
What You'll Build
- A Remix
_healthroute that probes real dependencies - A Vigilmon monitor checking the route every 5 minutes from multiple regions
- Alert channels (email + Slack webhook) for instant down/up notifications
Prerequisites
- Node.js 18+ with a Remix project (or
npx create-remix@latest) - A free Vigilmon account (5 monitors free, no credit card)
Step 1: Create a Health-Check Route in Remix
Remix uses file-based routing. Create a dedicated health route that bypasses the React rendering pipeline entirely:
app/
routes/
_health.tsx ← new file
_index.tsx
// app/routes/_health.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
const health = {
status: "ok",
timestamp: new Date().toISOString(),
env: process.env.NODE_ENV,
};
return json(health, {
status: 200,
headers: {
"Cache-Control": "no-store",
},
});
}
// No default export — this route returns JSON only, no UI
The absence of a default export means Remix won't try to render a React component for this route; it's a pure data endpoint.
Test it:
npx remix dev
curl http://localhost:3000/_health
# {"status":"ok","timestamp":"...","env":"development"}
Why
_healthinstead ofhealth? The leading underscore tells Remix this is a "pathless" route segment — you access it at/_health. You can also usehealth.tsxfor/health— either works fine with Vigilmon.
Step 2: Add Real Dependency Checks
A 200-always endpoint only tells you the Node process is alive. Add checks that test your actual data layer:
// app/routes/_health.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { db } from "~/db.server"; // your Prisma / Drizzle client
import { redis } from "~/redis.server"; // your Redis client (if any)
interface CheckResult {
ok: boolean;
latencyMs?: number;
error?: string;
}
async function checkDatabase(): Promise<CheckResult> {
const start = Date.now();
try {
// Prisma example:
await db.$queryRaw`SELECT `4;
// Drizzle example:
// await db.execute(sql`SELECT 1`);
return { ok: true, latencyMs: Date.now() - start };
} catch (err) {
return { ok: false, error: String(err) };
}
}
async function checkRedis(): Promise<CheckResult> {
if (!redis) return { ok: true }; // skip if not configured
const start = Date.now();
try {
await redis.ping();
return { ok: true, latencyMs: Date.now() - start };
} catch (err) {
return { ok: false, error: String(err) };
}
}
export async function loader({ request }: LoaderFunctionArgs) {
const [database, cache] = await Promise.allSettled([
checkDatabase(),
checkRedis(),
]);
const dbResult = database.status === "fulfilled" ? database.value : { ok: false };
const cacheResult = cache.status === "fulfilled" ? cache.value : { ok: false };
const allOk = dbResult.ok && cacheResult.ok;
return json(
{
status: allOk ? "ok" : "degraded",
checks: { database: dbResult, cache: cacheResult },
timestamp: new Date().toISOString(),
},
{
status: allOk ? 200 : 503,
headers: { "Cache-Control": "no-store" },
}
);
}
Vigilmon sees the 503 and fires an alert — no manual threshold configuration needed.
Step 3: Deploy Your Remix App
Remix runs on multiple targets. The health route works identically across all of them.
Vercel (zero config):
npm install @remix-run/vercel vercel
npx vercel deploy --prod
Fly.io (Dockerfile-based):
fly launch # generates fly.toml + Dockerfile
fly deploy
Railway / Render: Connect your GitHub repo and set the start command to npm run start.
After deployment, note your production URL, e.g. https://my-remix-app.vercel.app.
Step 4: Create the Vigilmon Monitor
- Log in at vigilmon.online and click + New Monitor.
- Monitor Type: HTTP / HTTPS.
-
URL:
https://my-remix-app.vercel.app/_health - Check Interval: 5 minutes.
-
Expected Status: 2xx (or
200specifically). - Click Save Monitor.
Vigilmon immediately begins polling from geographically distributed nodes. The multi-region consensus model means a single regional network fault won't trigger a false alert — it only fires when a majority of probes agree the site is unreachable.
Step 5: Configure Alert Channels
Settings → Alert Channels → + Add Channel → Email
Enter your email address. Assign it to the monitor under Monitors → Edit → Alert Channels. You'll receive:
- A "DOWN" email within minutes of the first failed check consensus
- An "UP" email once the endpoint recovers, with total downtime duration
Slack Webhook
- In your Slack workspace: Apps → Incoming Webhooks → Add to Slack, pick a channel, copy the URL.
- In Vigilmon: Settings → Alert Channels → + Add Channel → Webhook, paste the URL.
- Assign it to your monitor.
Sample down alert your Slack channel receives:
{
"monitor": "my-remix-app /_health",
"status": "down",
"reason": "HTTP 503",
"body_excerpt": "{\"status\":\"degraded\",\"checks\":{\"database\":{\"ok\":false}}}",
"regions_failed": ["us-east", "eu-west"],
"checked_at": "2024-01-15T10:31:00Z"
}
Step 6: Multi-Region Strategy for Remix
Remix on edge runtimes (Cloudflare Workers, Vercel Edge) is often deployed to tens of regions simultaneously. Consider adding region-aware health metadata:
export async function loader({ request }: LoaderFunctionArgs) {
// Cloudflare: request.cf?.colo; Vercel: process.env.VERCEL_REGION
const region =
(request as any).cf?.colo ??
process.env.VERCEL_REGION ??
process.env.FLY_REGION ??
"unknown";
const allOk = true; // replace with your checks
return json(
{ status: allOk ? "ok" : "degraded", region, timestamp: new Date().toISOString() },
{ status: allOk ? 200 : 503, headers: { "Cache-Control": "no-store" } }
);
}
If you use Remix on multiple custom domains (e.g. per-tenant subdomains), add a separate Vigilmon monitor for each — the free tier covers up to 5 monitors.
Step 7: Test the Full Pipeline
Simulate a failure by temporarily forcing a 503:
// _health.tsx — temporary, for testing only
export async function loader() {
return json({ status: "down", reason: "test failure" }, { status: 503 });
}
Deploy, wait ≥ 5 minutes, and confirm the alert fires. Then revert and confirm the recovery notification.
Step 8: Add a Status Badge (Optional)
Show your uptime status directly in your README:
[](https://vigilmon.online)
Find your monitor ID in the dashboard URL. The badge auto-updates on every check cycle.
Remix-Specific Tips
Loader caching: Always set Cache-Control: no-store on health responses. Remix loaders are sometimes cached by CDNs — a cached 200 will mask a real outage.
Resource routes vs. standard routes: The approach above uses a resource route (no default export). This is the recommended pattern — it keeps health checks off the React rendering critical path and avoids any risk of client-side hydration errors.
Nested routes: If your app uses a root loader that fetches user session data, don't inherit it in the health route. Keep health checks stateless and fast.
Scheduled jobs / background queues: If your Remix app depends on a worker process (Inngest, BullMQ), add a separate endpoint or Vigilmon monitor for the queue health — the web health route won't catch a stalled job processor.
Summary
You've added production-grade monitoring to your Remix application in a few steps:
- A
_healthresource route that tests real dependencies and returns 503 on failure - Vigilmon polling every 5 minutes from multiple global regions
- Email and Slack webhook alerts with automatic recovery notifications
The Vigilmon free tier at vigilmon.online requires no credit card and supports up to 5 monitors — perfect for a Remix project in early production.
Using Remix in an interesting stack? Let me know in the comments what you're monitoring.
Top comments (0)