tutorial

Monitoring Convex with Vigilmon

Convex is a reactive backend platform — but function execution latency spikes, slow database queries, stalled real-time subscriptions, failed scheduler jobs, and bandwidth overages silently degrade your application. Here's how to monitor Convex function health, query performance, subscription status, and scheduler reliability with Vigilmon.

Convex is a reactive backend-as-a-service platform that gives you TypeScript functions, a document database, real-time subscriptions, file storage, and a built-in scheduler in a single managed platform. Your frontend queries and mutations run as serverless Convex functions; the database's live query system pushes updates to subscribed clients the moment data changes. When a Convex function starts timing out under load, when a database query scans too many documents and hits the execution time limit, when a real-time subscription silently disconnects and clients stop receiving updates, or when a scheduled job that drives critical background work stalls — your application degrades in ways that are hard to detect from the outside. Vigilmon gives you external monitoring for Convex function health, database query performance, real-time subscription connectivity, scheduler job reliability, and bandwidth utilization so you catch backend issues before they impact your users.

What You'll Set Up

  • HTTP endpoint health monitoring for Convex HTTP actions
  • Function execution latency tracking
  • Database query performance monitoring
  • Real-time subscription connectivity checks
  • Scheduler job status monitoring

Prerequisites

  • A Convex project with at least one deployed HTTP action or query
  • Convex CLI (npx convex) and a deployment URL
  • Access to Convex dashboard metrics (for advanced monitoring)
  • A free Vigilmon account

Step 1: Monitor Your Convex HTTP Actions

Convex HTTP actions are the most accessible external monitoring surface — they're standard HTTP endpoints that accept requests without requiring Convex client authentication. If your application exposes any HTTP actions, start by adding Vigilmon monitors for them.

  1. In Vigilmon, click Add MonitorHTTP / HTTPS.
  2. Enter your HTTP action URL: https://your-deployment.convex.site/api/health.
  3. Set Check interval to 1 minute.
  4. Set Expected HTTP status to 200.
  5. Click Save.

If you don't have an HTTP action health endpoint yet, add one in your Convex project:

// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";

const http = httpRouter();

http.route({
  path: "/health",
  method: "GET",
  handler: httpAction(async (ctx, _request) => {
    // Run a lightweight query to verify database connectivity
    const docCount = await ctx.runQuery(
      internal.health.checkDatabaseConnectivity
    );
    return new Response(
      JSON.stringify({ status: "ok", db_reachable: true, sample_count: docCount }),
      { status: 200, headers: { "Content-Type": "application/json" } }
    );
  }),
});

export default http;
// convex/health.ts
import { internalQuery } from "./_generated/server";

export const checkDatabaseConnectivity = internalQuery({
  handler: async (ctx) => {
    // Read one document from a non-sensitive table to verify DB is responding
    const docs = await ctx.db.query("_scheduled_functions").take(1);
    return docs.length;
  },
});

Deploy with npx convex deploy. The /health endpoint is now at https://your-deployment.convex.site/health. Add it to Vigilmon for continuous uptime monitoring with 1-minute checks.


Step 2: Track Function Execution Latency

Convex functions — queries, mutations, and actions — execute within time limits (queries and mutations have strict limits; actions have longer ones). When execution time climbs toward the limit, Convex starts returning timeout errors to clients. Monitor function latency by exposing it from a probe endpoint:

// convex/http.ts (add to existing router)
http.route({
  path: "/latency-probe",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const start = Date.now();

    // Run a representative query
    const result = await ctx.runQuery(internal.latencyProbe.sampleQuery);

    const latencyMs = Date.now() - start;
    const threshold = parseInt(
      new URL(request.url).searchParams.get("threshold") ?? "500"
    );

    if (latencyMs > threshold) {
      return new Response(
        JSON.stringify({ status: "slow", latency_ms: latencyMs, threshold }),
        { status: 503, headers: { "Content-Type": "application/json" } }
      );
    }

    return new Response(
      JSON.stringify({ status: "ok", latency_ms: latencyMs }),
      { status: 200, headers: { "Content-Type": "application/json" } }
    );
  }),
});
// convex/latencyProbe.ts
import { internalQuery } from "./_generated/server";

export const sampleQuery = internalQuery({
  handler: async (ctx) => {
    // Use a table and index that your production queries actually use
    return await ctx.db.query("messages").order("desc").take(10);
  },
});

Add a Vigilmon HTTP monitor for https://your-deployment.convex.site/latency-probe?threshold=500. Set Expected HTTP status to 200 — a 503 response means latency exceeded the threshold and will trigger an alert.

Run this check every 2 minutes. A sudden latency increase usually indicates a missing index on a growing table (causing full table scans), a mutation creating lock contention, or an upstream data provider an action depends on becoming slow.


Step 3: Monitor Database Query Performance

Convex queries run within strict execution time limits, and the most common cause of approaching those limits is missing indexes on large tables. When a query that previously ran in milliseconds starts scanning thousands of documents (because the table grew past a threshold), it can hit the execution limit and return errors to all subscribed clients simultaneously. Monitor query performance patterns:

// convex/http.ts
http.route({
  path: "/db-health",
  method: "GET",
  handler: httpAction(async (ctx, _request) => {
    const checks = await ctx.runQuery(internal.dbHealth.runChecks);
    const allOk = checks.every((c) => c.ok);

    return new Response(JSON.stringify({ status: allOk ? "ok" : "degraded", checks }), {
      status: allOk ? 200 : 503,
      headers: { "Content-Type": "application/json" },
    });
  }),
});
// convex/dbHealth.ts
import { internalQuery } from "./_generated/server";

export const runChecks = internalQuery({
  handler: async (ctx) => {
    const results = [];

    // Check 1: Most recent document age (detect stalled writes)
    const latestMessage = await ctx.db
      .query("messages")
      .withIndex("by_creation_time")
      .order("desc")
      .first();

    const messageAge = latestMessage
      ? Date.now() - latestMessage._creationTime
      : null;

    results.push({
      name: "latest_message_age",
      ok: messageAge === null || messageAge < 3600000, // < 1 hour
      value_ms: messageAge,
    });

    // Check 2: Pending scheduled functions (detect scheduler backlog)
    const pending = await ctx.db
      .query("_scheduled_functions")
      .withIndex("by_state", (q) => q.eq("state.kind", "pending"))
      .collect();

    results.push({
      name: "scheduler_backlog",
      ok: pending.length < 100,
      count: pending.length,
    });

    return results;
  },
});

Add a Vigilmon HTTP monitor for https://your-deployment.convex.site/db-health. A 503 response with degraded status indicates database query issues and triggers an immediate alert.


Step 4: Verify Real-Time Subscription Health

Convex's live query system is one of its most powerful features — clients subscribe to queries and receive push updates when the underlying data changes. But WebSocket connectivity issues, Convex deployment restarts, or client-side bugs can cause subscriptions to silently stall: the client stays connected but stops receiving updates. Build a subscription health check using Convex's HTTP actions and a lightweight polling check:

// convex/http.ts
http.route({
  path: "/subscription-probe",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    // Write a probe document and verify it appears in the queryable state
    const probeId = await ctx.runMutation(internal.subscriptionProbe.writeProbe);
    const verified = await ctx.runQuery(
      internal.subscriptionProbe.verifyProbe,
      { probeId }
    );

    // Clean up
    await ctx.runMutation(internal.subscriptionProbe.deleteProbe, { probeId });

    return new Response(
      JSON.stringify({ status: verified ? "ok" : "stale", probe_id: probeId }),
      {
        status: verified ? 200 : 503,
        headers: { "Content-Type": "application/json" },
      }
    );
  }),
});
// convex/subscriptionProbe.ts
import { internalMutation, internalQuery } from "./_generated/server";
import { v } from "convex/values";

export const writeProbe = internalMutation({
  handler: async (ctx) => {
    return await ctx.db.insert("_health_probes", {
      timestamp: Date.now(),
      type: "subscription_probe",
    });
  },
});

export const verifyProbe = internalQuery({
  args: { probeId: v.id("_health_probes") },
  handler: async (ctx, { probeId }) => {
    const doc = await ctx.db.get(probeId);
    return doc !== null;
  },
});

export const deleteProbe = internalMutation({
  args: { probeId: v.id("_health_probes") },
  handler: async (ctx, { probeId }) => {
    await ctx.db.delete(probeId);
  },
});

This probe writes a document and immediately queries it back, verifying that the mutation and query path are both working. Add a Vigilmon HTTP monitor using the POST method:

  1. In Vigilmon, click Add MonitorHTTP / HTTPS.
  2. Enter URL: https://your-deployment.convex.site/subscription-probe.
  3. Set Method to POST.
  4. Set Check interval to 5 minutes.
  5. Set Expected HTTP status to 200.

Step 5: Monitor Scheduler Job Status

Convex's built-in scheduler lets you run functions at specific times or after delays — a powerful primitive for background jobs, retries, and deferred work. If a scheduled function throws an unhandled error, Convex marks it as failed and does not retry it by default. A long-running background job that silently fails means your users stop receiving emails, notifications, or processed data with no visible error.

Monitor scheduler health via a dedicated probe:

// convex/http.ts
http.route({
  path: "/scheduler-health",
  method: "GET",
  handler: httpAction(async (ctx, _request) => {
    const health = await ctx.runQuery(internal.schedulerHealth.check);

    return new Response(JSON.stringify(health), {
      status: health.ok ? 200 : 503,
      headers: { "Content-Type": "application/json" },
    });
  }),
});
// convex/schedulerHealth.ts
import { internalQuery } from "./_generated/server";

export const check = internalQuery({
  handler: async (ctx) => {
    const now = Date.now();
    const oneHourAgo = now - 3600000;

    // Count recently failed scheduled functions
    const failed = await ctx.db
      .query("_scheduled_functions")
      .withIndex("by_state", (q) => q.eq("state.kind", "failed"))
      .filter((q) => q.gte(q.field("scheduledTime"), oneHourAgo))
      .collect();

    // Count overdue pending functions (scheduled > 5 min ago, still pending)
    const fiveMinutesAgo = now - 300000;
    const overdue = await ctx.db
      .query("_scheduled_functions")
      .withIndex("by_state", (q) => q.eq("state.kind", "pending"))
      .filter((q) => q.lte(q.field("scheduledTime"), fiveMinutesAgo))
      .collect();

    return {
      ok: failed.length === 0 && overdue.length === 0,
      failed_count: failed.length,
      overdue_count: overdue.length,
      checked_at: now,
    };
  },
});

Add a Vigilmon HTTP monitor for https://your-deployment.convex.site/scheduler-health with a 5-minute check interval. A 503 response means either scheduled jobs are failing or the queue has a backlog — both warrant immediate investigation.


Putting It All Together

With these five monitors active, your Vigilmon dashboard covers every critical layer of your Convex backend:

| Monitor | Type | Check Interval | Alert On | |---------|------|----------------|----------| | HTTP action health | HTTP | 1 min | Non-200 response | | Function execution latency | HTTP | 2 min | Latency > 500ms | | Database query health | HTTP | 5 min | Degraded status | | Subscription probe | HTTP | 5 min | Write/read failure | | Scheduler job status | HTTP | 5 min | Failed or overdue jobs |

The most impactful monitor to set up first is the HTTP action health check — it covers the full Convex function-to-database path in a single check and is the fastest to implement. Follow it with the scheduler health check if your application relies on background jobs for critical workflows like notifications or payment processing.

Convex's managed infrastructure handles server reliability, database replication, and real-time WebSocket infrastructure for you — but application-level health (query performance, scheduler correctness, subscription connectivity) is your responsibility. External monitoring with Vigilmon closes that gap, giving you the signal you need to catch degradation before your users do.

Sign up for a free Vigilmon account and add your first Convex HTTP monitor in under two minutes.

Monitor your app with Vigilmon

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

Start free →