All posts

August 20, 2026 6 min read

How to monitor Render cron jobs

Render sends failure alerts when a cron job exits non-zero — but no alert when a job never starts. Here's how to close that gap with external monitoring.


Render's cron jobs are a significant improvement over Heroku Scheduler: they're a first-class service type rather than an add-on, they run on dedicated compute rather than one-off dynos, and Render guarantees that at most one instance of each job runs at a time — if a previous run is still active, the next execution is delayed rather than skipped.

Render also sends failure notifications when a job exits non-zero, which Heroku Scheduler doesn't do by default.

But there's a gap that Render's documentation acknowledges: Render has no "did-not-fire" alert. When a scheduled job simply doesn't start — because of a Render platform issue, a deployment that corrupted the service configuration, or the job hanging and blocking subsequent runs — no notification fires. From outside the Render dashboard, you have no signal that the job stopped running until a downstream effect surfaces.


What Render monitors and what it doesn't

What Render does: sends an email or Slack notification (if configured under Integrations) when a cron job execution exits with a non-zero exit code. This catches explicit failures — jobs that crash, jobs that call sys.exit(1), jobs that raise an unhandled exception.

What Render doesn't do:

  • Alert when a job doesn't start at all
  • Alert when a job runs but produces no useful output (exit code 0 with zero records processed)
  • Alert when a job exceeds an expected maximum duration
  • Provide a historical view of execution timing to surface gradual drift

The absence of a "did-not-fire" alert means that if Render's scheduler stops triggering your job — or if your job hangs and Render delays subsequent runs indefinitely — you won't know until the data is stale or a user complains.


Adding external monitoring to Render cron jobs

The pattern is the same as any cron service: your job sends HTTP pings to an external monitoring service at start and completion. The external service holds the expected schedule and fires an alert if pings don't arrive on time.

For a Node.js Render cron service:

import { CrontifyMonitor } from '@crontify/sdk';

const monitor = new CrontifyMonitor({
  apiKey: process.env.CRONTIFY_API_KEY!,
  monitorId: process.env.CRONTIFY_MONITOR_ID!,
});

async function main(): Promise<void> {
  await monitor.wrap(async () => {
    const result = await runJob();
    return {
      meta: {
        records_processed: result.count,
        duration_ms: result.durationMs,
      }
    };
  });
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

For a Python Render cron service:

import os
import sys
import requests
import traceback

MONITOR_ID = os.environ["CRONTIFY_MONITOR_ID"]
API_KEY = os.environ["CRONTIFY_API_KEY"]
BASE_URL = f"https://api.crontify.com/api/v1/ping/{MONITOR_ID}"
HEADERS = {"X-API-Key": API_KEY}


def ping(event: str, payload: dict | None = None) -> None:
    try:
        requests.post(f"{BASE_URL}/{event}", headers=HEADERS,
                      json=payload, timeout=10)
    except Exception:
        pass


def main() -> None:
    ping("start")
    try:
        result = run_job()
        ping("success", {
            "meta": {
                "records_processed": result.count,
                "duration_ms": result.duration_ms,
            }
        })
    except Exception as exc:
        ping("fail", {
            "message": str(exc),
            "log": traceback.format_exc(),
        })
        sys.exit(1)


if __name__ == "__main__":
    main()

Add CRONTIFY_API_KEY and CRONTIFY_MONITOR_ID as environment variables in your Render service settings.


Render's delayed execution behaviour and grace periods

Render delays the next run if the previous one is still active, rather than skipping it. This means that if your job occasionally takes longer than its interval, runs can stack up and execute late. A daily job that normally takes 30 minutes but occasionally takes 26 hours will delay the next day's run until the long-running job completes.

When configuring your monitor's grace period, account for this. For a daily job that normally takes 30 minutes, a grace period of 2–4 hours is safer than 30 minutes — a legitimately long run will delay the next start ping past a shorter grace period and trigger a false alarm.

A more reliable approach: configure the grace period based on the maximum acceptable delay, not the typical runtime. If you need the daily sync to run within 4 hours of its scheduled time, use a 4-hour grace period. If a Render platform issue delays it beyond that threshold, you want to know.


Catching the exit-0-but-did-nothing failure

Render's failure notifications fire on non-zero exit codes. They don't fire when your job exits 0 after processing nothing useful.

This is where alert rules on job output metadata make the difference. When your job calls the success ping, it attaches what it actually did:

# In a bash Render cron job
ping_success() {
  curl -fsS -X POST \
    "https://api.crontify.com/api/v1/ping/${CRONTIFY_MONITOR_ID}/success" \
    -H "X-API-Key: ${CRONTIFY_API_KEY}" \
    -H "Content-Type: application/json" \
    -d "{\"meta\":{\"files_synced\":${FILE_COUNT},\"backup_bytes\":${BACKUP_SIZE}}}" \
    > /dev/null 2>&1 || true
}

In Crontify's dashboard, define rules:

  • files_synced eq 0 → alert
  • backup_bytes lt 1000 → alert

Render sees a zero exit code. Crontify sees zero files synced and fires an alert.


What you get

After adding external monitoring to your Render cron jobs:

  • Did-not-fire alerts — the gap Render's native tooling leaves open. When no start ping arrives within the grace period, you know within minutes.
  • Hung job detection — start ping with no success ping within your configured threshold. Catches jobs that run longer than expected without timing out.
  • Silent failure detection — jobs that exit 0 but produce nothing useful. Available through alert rules on output metadata.
  • Log context on failure — up to 10,000 characters of error output delivered inline in the Slack or email alert, on top of Render's own failure notification.
  • Recovery alerts — notification when a previously failing job returns to healthy.

Crontify is free for up to 5 monitors — no credit card required.


Start monitoring your scheduled jobs

Free plan includes 5 monitors. No credit card required. Up and running in under 5 minutes.

Get started free →