Мониторинг node-cron и других планировщиков Node.js

Обновлено 3 ноября 2026

С библиотеками вроде node-cron, node-schedule, croner или @nestjs/schedule так и хочется держать задачи по расписанию прямо в приложении на Node.js: ни crontab, ни отдельного контейнера, задача использует код и настройки приложения. Подвох в том, что расписание живёт ровно столько, сколько процесс, а ошибка внутри колбэка в лучшем случае превращается в строку в логе.

Что ломается в планировщиках внутри процесса

Оберните каждую задачу

Этот помощник работает с любым планировщиком. Ему нужен Node.js 18 или новее ради встроенного fetch. Он сообщает о старте, результате и ошибке, пропускает запуск, пока идёт предыдущий, и не даёт неудачному пингу сломать задачу:

// monitored.js
async function ping(url, body) {
  try {
    await fetch(url, { method: body ? "POST" : "GET", body, signal: AbortSignal.timeout(10_000) });
  } catch {
    // мониторинг не должен ломать задачу
  }
}

export function monitored(url, job) {
  let running = false;
  return async () => {
    if (running) return; // предыдущий запуск ещё идёт
    running = true;
    await ping(`${url}/start`);
    try {
      await job();
      await ping(url);
    } catch (err) {
      await ping(`${url}/fail`, String(err?.stack ?? err).slice(0, 100_000));
    } finally {
      running = false;
    }
  };
}

Ошибки перехватываются внутри обёртки, поэтому unhandled rejection не уронит приложение, а текст ошибки появится в журнале событий Heartcron.

node-cron

import cron from "node-cron";
import { monitored } from "./monitored.js";

cron.schedule(
  "0 3 * * *",
  monitored("https://ping.heartcron.com/<uuid>", sendDailyDigest),
  { timezone: "Europe/Moscow" },
);

NestJS

import { Injectable } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { monitored } from "./monitored";

@Injectable()
export class ReportsService {
  private readonly digest = monitored("https://ping.heartcron.com/<uuid>", () => this.sendDailyDigest());

  @Cron("0 3 * * *", { timeZone: "Europe/Moscow" })
  run() {
    return this.digest();
  }
}

В Heartcron создайте проверку с тем же выражением 0 3 * * * и тем же часовым поясом. Запас времени сделайте чуть больше обычной длительности запуска. Если в 3:00 приложение лежало, пинг не придёт, и вы об этом узнаете.

Один планировщик, а не по одному на реплику

Проверить это помогает журнал событий Heartcron: если один запуск по расписанию приходит несколькими пингами с разницей в миллисекунды, его запускает больше одного процесса.

Или вынесите задачу из процесса

Задачу, которая должна выполняться даже при упавшем приложении, лучше сделать отдельным скриптом, который запускает системный cron, планировщик контейнеров или cron платформы. Тогда открытая утилита heartcron обернёт её без помощника выше:

0 3 * * * heartcron run daily-digest -- node /srv/app/scripts/digest.js

Она отправит старт, код завершения, длительность и последние строки вывода. Что значит выражение, можно проверить в расшифровщике cron-выражений.

Другие руководства