Cron Monitoring Setup

Monitor scheduled jobs and background tasks with check-ins, timeout detection, and failure alerts.

What is Cron Monitoring?#

Cron monitoring tracks the execution of scheduled tasks, background jobs, and recurring processes in your application. It detects three types of failures:

  • Missed check-ins -- your job did not run when expected
  • Timeouts -- your job started but did not complete within the expected time
  • Failures -- your job ran but reported an error

Unlike traditional uptime monitoring that checks external endpoints, cron monitoring works from inside your application -- your code reports check-ins to JustAnalytics, and the system alerts you when something goes wrong.

How It Works#

Cron monitoring uses a check-in flow:

Your Job Starts → Send "in_progress" check-in → Job runs → Send "ok" or "error" check-in

If JustAnalytics does not receive an in_progress check-in within the expected schedule window, it marks the job as missed. If it receives in_progress but no completion (ok or error) within the timeout period, it marks the job as timed out.

Prerequisites#

Install the Node.js SDK:

npm install @justanalyticsapp/node

Initialize the SDK in your application:

import JA from '@justanalyticsapp/node';

JA.init({
  siteId: 'YOUR_SITE_ID',
  apiKey: 'YOUR_API_KEY',
  serviceName: 'my-worker',
});

Creating a Cron Monitor#

In the Dashboard#

  1. Navigate to Monitoring > Cron Monitors
  2. Click Create Monitor
  3. Configure the monitor:

| Field | Description | Example | |-------|-------------|---------| | Name | Descriptive name for the job | daily-report-generation | | Slug | URL-safe identifier used in API calls | daily-report-generation | | Schedule | Cron expression or interval | 0 2 * * * (daily at 2am) | | Timezone | Timezone for the schedule | America/New_York | | Grace Period | Minutes to wait before marking as missed | 5 | | Timeout | Minutes before marking an in-progress job as timed out | 30 | | Alert Channels | Where to send failure notifications | Email, Slack |

  1. Click Save and copy the generated monitor slug

Cron Expression Reference#

| Expression | Meaning | |------------|---------| | * * * * * | Every minute | | */5 * * * * | Every 5 minutes | | 0 * * * * | Every hour | | 0 */6 * * * | Every 6 hours | | 0 2 * * * | Daily at 2:00 AM | | 0 9 * * 1-5 | Weekdays at 9:00 AM | | 0 0 * * 0 | Weekly on Sunday at midnight | | 0 0 1 * * | Monthly on the 1st at midnight |

Sending Check-Ins#

Basic Check-In Flow#

import JA from '@justanalyticsapp/node';

async function dailyReportJob() {
  // Signal that the job has started
  const checkInId = await JA.cronCheckIn('daily-report-generation', {
    status: 'in_progress',
  });

  try {
    // Your job logic
    const report = await generateDailyReport();
    await sendReportEmail(report);

    // Signal successful completion
    await JA.cronCheckIn('daily-report-generation', {
      status: 'ok',
      checkInId, // Links to the in_progress check-in
      duration: Date.now() - startTime,
    });
  } catch (error) {
    // Signal failure
    await JA.cronCheckIn('daily-report-generation', {
      status: 'error',
      checkInId,
      message: error.message,
    });
    throw error; // Re-throw so your job runner knows it failed
  }
}

Simplified Wrapper#

Use JA.withCronMonitor() for a cleaner syntax that handles the in_progress/ok/error flow automatically:

import JA from '@justanalyticsapp/node';

const result = await JA.withCronMonitor('daily-report-generation', async () => {
  const report = await generateDailyReport();
  await sendReportEmail(report);
  return report;
});

// If the function throws, status is set to 'error' automatically
// If it completes, status is set to 'ok' automatically
// Duration is measured automatically

Check-In API Reference#

JA.cronCheckIn(monitorSlug: string, options: {
  status: 'in_progress' | 'ok' | 'error';
  checkInId?: string;     // Required for 'ok' and 'error' to link to 'in_progress'
  duration?: number;      // Duration in milliseconds
  message?: string;       // Optional message (e.g., error message or summary)
  environment?: string;   // Override the default environment
}): Promise<string>;      // Returns checkInId

Grace Periods and Timeouts#

Grace Period#

The grace period is how long JustAnalytics waits after the expected run time before marking the job as missed. This accounts for slight delays in job scheduling.

Schedule: 0 2 * * * (daily at 2:00 AM)
Grace Period: 5 minutes

Expected check-in window: 2:00 AM - 2:05 AM
If no check-in by 2:05 AM → marked as MISSED

Set a longer grace period for jobs that may be delayed by queue congestion or server load:

  • Short jobs (< 1 min): 2-5 minute grace period
  • Medium jobs (1-10 min): 5-15 minute grace period
  • Long-running jobs (> 10 min): 15-30 minute grace period
  • Jobs in autoscaling environments: 10-30 minute grace period

Timeout#

The timeout is how long JustAnalytics waits after receiving an in_progress check-in before marking the job as timed out.

In-progress received at: 2:01 AM
Timeout: 30 minutes

If no 'ok' or 'error' by 2:31 AM → marked as TIMED OUT

Set the timeout based on your job's expected maximum duration plus a buffer:

  • If the job normally takes 5 minutes, set timeout to 15 minutes
  • If the job normally takes 30 minutes, set timeout to 60 minutes
  • If the job has variable duration, use the 99th percentile plus 50%

Alert Rules for Cron Monitors#

Default Alerts#

By default, cron monitors send alerts for:

  • Missed check-in -- the job did not start when expected
  • Timeout -- the job started but did not complete
  • Error -- the job reported a failure

Custom Alert Rules#

Create custom rules for more nuanced alerting:

{
  "name": "Report Job Consecutive Failures",
  "monitor": "daily-report-generation",
  "condition": "consecutive_failures",
  "threshold": 3,
  "channels": ["slack", "pagerduty"],
  "message": "Daily report has failed 3 times in a row"
}
{
  "name": "Slow Sync Job",
  "monitor": "hourly-data-sync",
  "condition": "duration_exceeds",
  "threshold_ms": 300000,
  "channels": ["email"],
  "message": "Hourly sync took longer than 5 minutes"
}

Alert Channels#

Cron monitor alerts can be sent to:

  • Email -- sent to site owner and configured team members
  • Slack -- posted to a configured Slack channel via webhook
  • Webhook -- sent as a POST request to any URL
  • PagerDuty -- creates an incident via PagerDuty integration

Configure channels in Settings > Notification Channels.

Code Examples for Common Patterns#

Daily Database Cleanup#

import JA from '@justanalyticsapp/node';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Runs daily at 3:00 AM
async function cleanupOldRecords() {
  await JA.withCronMonitor('daily-db-cleanup', async () => {
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - 90);

    const deleted = await prisma.tempFile.deleteMany({
      where: { createdAt: { lt: cutoff } },
    });

    JA.logger.info('Database cleanup completed', {
      deletedRecords: deleted.count,
      cutoffDate: cutoff.toISOString(),
    });

    return { deleted: deleted.count };
  });
}

Hourly Data Sync#

import JA from '@justanalyticsapp/node';

// Runs every hour
async function syncExternalData() {
  await JA.withCronMonitor('hourly-data-sync', async () => {
    const sources = ['inventory-api', 'pricing-api', 'shipping-api'];
    const results = [];

    for (const source of sources) {
      const data = await fetchFromSource(source);
      await updateLocalDatabase(source, data);
      results.push({ source, records: data.length });
    }

    JA.logger.info('Data sync completed', { results });
    return results;
  });
}

Weekly Report Generation#

import JA from '@justanalyticsapp/node';

// Runs every Monday at 8:00 AM
async function generateWeeklyReport() {
  await JA.withCronMonitor('weekly-report', async () => {
    const report = await buildReport({
      startDate: getLastMonday(),
      endDate: getLastSunday(),
      metrics: ['revenue', 'orders', 'new_users', 'churn'],
    });

    await sendToSlack(report.summary);
    await sendByEmail(report.full, ['team@company.com']);
    await archiveReport(report);

    JA.logger.info('Weekly report sent', {
      period: `${report.startDate} to ${report.endDate}`,
      recipients: report.recipientCount,
    });
  });
}

Queue Worker Health Check#

For long-running queue workers that process jobs continuously, use periodic heartbeat check-ins:

import JA from '@justanalyticsapp/node';

// Worker that processes jobs from a queue
async function startQueueWorker() {
  const HEARTBEAT_INTERVAL = 5 * 60 * 1000; // 5 minutes
  let jobsProcessed = 0;
  let lastHeartbeat = Date.now();

  while (true) {
    const job = await queue.dequeue();

    if (job) {
      await processJob(job);
      jobsProcessed++;
    }

    // Send heartbeat every 5 minutes
    if (Date.now() - lastHeartbeat > HEARTBEAT_INTERVAL) {
      await JA.cronCheckIn('queue-worker-heartbeat', {
        status: 'ok',
        message: `Processed ${jobsProcessed} jobs since last heartbeat`,
      });
      jobsProcessed = 0;
      lastHeartbeat = Date.now();
    }
  }
}

Express.js Cron with node-cron#

import JA from '@justanalyticsapp/node';
import cron from 'node-cron';

// Schedule a job using node-cron
cron.schedule('0 */6 * * *', async () => {
  await JA.withCronMonitor('6h-cache-refresh', async () => {
    await refreshProductCache();
    await refreshCategoryCache();
    await refreshSearchIndex();

    JA.logger.info('Cache refresh completed');
  });
}, {
  timezone: 'America/New_York',
});

Bull/BullMQ Recurring Job#

import JA from '@justanalyticsapp/node';
import { Queue, Worker } from 'bullmq';

const queue = new Queue('scheduled-tasks');

// Add a recurring job
await queue.add('daily-digest', {}, {
  repeat: { cron: '0 9 * * *' },
});

// Worker with cron monitoring
const worker = new Worker('scheduled-tasks', async (job) => {
  if (job.name === 'daily-digest') {
    await JA.withCronMonitor('daily-digest-email', async () => {
      const users = await getActiveUsers();
      for (const user of users) {
        const digest = await buildDigest(user);
        await sendDigestEmail(user.email, digest);
      }

      JA.logger.info('Daily digest sent', { userCount: users.length });
    });
  }
});

Viewing Cron Monitor Status#

Dashboard Overview#

Navigate to Monitoring > Cron Monitors to see all your monitors:

| Monitor | Schedule | Last Check-In | Status | Duration | Next Expected | |---------|----------|---------------|--------|----------|---------------| | daily-report | 0 2 * * * | 2:01 AM | OK | 4m 23s | Tomorrow 2:00 AM | | hourly-sync | 0 * * * * | 11:00 AM | OK | 1m 12s | 12:00 PM | | weekly-report | 0 8 * * 1 | Mon 8:00 AM | OK | 12m 45s | Next Mon 8:00 AM | | queue-worker | Every 5m | 11:55 AM | OK | - | 12:00 PM | | nightly-cleanup | 0 3 * * * | 3:00 AM | ERROR | 0m 8s | Tomorrow 3:00 AM |

Status Colors#

| Status | Color | Meaning | |--------|-------|---------| | OK | Green | Last check-in completed successfully | | In Progress | Blue | Job is currently running | | Error | Red | Last check-in reported an error | | Missed | Orange | Expected check-in was not received | | Timed Out | Yellow | Job started but did not complete | | Disabled | Gray | Monitor is paused |

Check-In History#

Click any monitor to see its check-in history:

  • Timeline of check-ins with status and duration
  • Duration trend chart (line chart showing job duration over time)
  • Success/failure rate over time
  • Error messages from failed runs
  • Link to associated logs and traces (if SDK logging is enabled)

Best Practices#

  1. Use withCronMonitor wrapper -- it handles the in_progress/ok/error flow and catches exceptions automatically
  2. Set realistic timeouts -- base them on observed job duration, not best-case
  3. Log within monitored jobs -- use JA.logger inside your jobs so logs are correlated with check-ins
  4. Monitor queue workers -- use heartbeat check-ins for long-running processes
  5. Name monitors descriptively -- use names like daily-report-generation not cron-1
  6. Test alert channels -- send a test alert when setting up a new monitor to verify delivery
  7. Review duration trends -- gradually increasing duration may indicate a growing dataset or performance issue

Troubleshooting#

Check-In Not Received#

  1. Verify the monitor slug matches exactly (case-sensitive)
  2. Check that the SDK is initialized with the correct siteId and apiKey
  3. Look for network errors in your application logs
  4. Verify the SDK can reach https://justanalytics.app/api/ingest/cron

False Missed Alerts#

  1. Increase the grace period to account for scheduling delays
  2. Check if the server's clock is synchronized (NTP)
  3. Verify the timezone setting matches your cron scheduler's timezone

False Timeout Alerts#

  1. Increase the timeout to account for worst-case job duration
  2. Check if the job is actually hanging (review application logs)
  3. Ensure the ok or error check-in is sent even when the job exits unexpectedly (use withCronMonitor or a finally block)

Next Steps#