Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A ten-minute Node.js poller can alert on recorded game-job failures, but polling alone cannot prove that a job which produced no log ever started. The dependable design combines four signals: an error/log API cursor, node-cron lifecycle events, start-and-finish heartbeats, and a webhook delivery queue with retries and idempotency.

On each interval, the worker reads records newer than its last cursor (with a small overlap), deduplicates them, applies a player-impact threshold, sends a structured alert, and advances the cursor only after a successful query. A late heartbeat covers silent stoppage, while an external process monitor handles crashes that Node.js cannot safely recover from inside the process.

What this alerting pattern detects—and what polling misses

Polling an error or log API is effective when the game service records an exception, timeout, failed deployment hook, or other explicit event. The poller groups records by game, job, error class, and occurrence window, then emits one actionable notification instead of one message per log line.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

There is a critical blind spot: a process crash, removed schedule, disabled worker, or deployment that prevents startup may create no new error record. Pair the poller with a heartbeat sent at job start and completion. If the expected start or completion check-in is late, alert on the missing signal rather than waiting for an error record that will never arrive.

The ten-minute interval is an operating choice, not a published reliability statistic. Use a shorter interval for jobs whose player impact rises quickly, and a longer one when the provider API is rate-limited or the job is low urgency.

Reference architecture

Component Responsibility State to retain
node-cron scheduler Starts one polling run every ten minutes and exposes missed, failed, and overlapping execution signals. Task name, run ID, scheduled time
Log/error API client Reads records after the last cursor, using a bounded timeout and retry policy. Cursor, query window, provider event IDs
Normalizer and threshold Converts provider-specific records into stable keys and decides when an alert is warranted. First seen, latest seen, count, error class
Delivery worker Sends a webhook, chat, or email request and retries transient failures. Idempotency key, attempts, response status
Heartbeat endpoint Receives start and completion check-ins so silent stoppage is visible. Expected schedule, last start, last completion, duration
Durable store Separates query progress from notification progress. Cursor, delivery status, dedupe key, retention time

For more than one Node.js instance, use a named distributed task or an external lock so only one instance polls a given interval. Configure noOverlap when a run can exceed its interval, and set an explicit IANA timezone instead of relying on the host setting. The scheduling options are documented at nodecron.com scheduling options.

Prepare the worker and its data model

Required inputs

  • An API credential with read access to game errors or logs.
  • A webhook, chat endpoint, or mail gateway that accepts the alert payload.
  • Durable storage for a cursor and delivery records; process memory is not sufficient after a restart.
  • A heartbeat receiver that can distinguish a start check-in from a completion check-in.
  • A runbook URL or identifier for the person receiving the alert.

Use a stable event key

Prefer the provider’s immutable event ID. If none exists, construct a deterministic key from the game identifier, job identifier, normalized error class, and a fixed occurrence window. Store the key with the latest occurrence and count. This prevents an overlapping query window from sending the same failure repeatedly.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep query and delivery state separate

Advance the cursor only after the API query has completed successfully and its records have been durably processed. Do not advance it merely because a webhook request was attempted. A delivery outage should leave a visible pending notification while allowing the next poll to continue from the correct position.

The ten-minute operating loop

  1. At minute 0, create a run ID and send a start heartbeat. Include the scheduled time, deployment version, host or instance ID, and expected completion deadline.
  2. Load the last successful cursor. Query from that cursor minus a small overlap. The overlap protects against provider indexing delay; deduplication makes it safe.
  3. Fetch and normalize records. Capture provider event ID when available, otherwise derive the stable key. Preserve the original timestamp and a redacted error message.
  4. Classify and threshold. Group by game and job, count occurrences, and decide whether one failure, a burst, or a sustained rate represents player impact.
  5. Deliver one structured alert per incident key. Include game, job, first-seen and latest-seen times, count, latest error, deployment version, run ID, and runbook reference.
  6. Persist the cursor after a successful query. Persist delivery status independently, including attempts and the last response status.
  7. Send a completion heartbeat. Include duration, number of records examined, number of incidents raised, and the cursor now stored.
  8. Escalate missing signals. Alert when the completion heartbeat is late, node-cron reports a failed or missed execution, an overlap is detected, or the process disappears.

Implementing a resilient poller

The following example uses a generic error API and webhook. Adapt the field names to the game service, but retain the ordering: timeout the request, normalize records, write durable state, then deliver.

import cron from 'node-cron';
import fetch from 'node-fetch';

const interval = '*/10 * * * *';
const overlapMs = 60_000;

function incidentKey(row) {
  if (row.id) return String(row.id);
  const bucket = Math.floor(new Date(row.occurredAt).getTime() / 300000);
  return [row.game, row.job, row.errorClass, bucket].join(':');
}

async function requestJson(url, options = {}, timeoutMs = 8000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetch(url, { ...options, signal: controller.signal });
  } catch (error) {
    if (error.name === 'AbortError') throw error;
    throw error;
  } finally {
    clearTimeout(timer);
  }
}

async function poll() {
  const runId = crypto.randomUUID();
  await heartbeat('start', { runId, scheduledAt: new Date().toISOString() });
  const state = await store.readPollState();
  const from = new Date(new Date(state.cursor).getTime() - overlapMs).toISOString();
  const response = await requestJson(`${process.env.GAME_ERROR_API}?from=${encodeURIComponent(from)}`);
  if (!response.ok) throw new Error(`error API returned ${response.status}`);
  const rows = await response.json();
  const incidents = aggregate(rows, incidentKey);

  for (const incident of incidents) {
    if (!passesThreshold(incident)) continue;
    const key = `game-failure:${incident.key}`;
    await deliverWithRetry(key, buildPayload(incident, runId));
  }

  const newest = newestTimestamp(rows, state.cursor);
  await store.writePollState({ cursor: newest, runId });
  await heartbeat('complete', { runId, count: rows.length, cursor: newest });
}

cron.schedule(interval, () => poll().catch(error => reportRunFailure(error)), {
  name: 'game-error-poller',
  noOverlap: true,
  timezone: 'UTC'
});

Use a real durable implementation for store, heartbeat, aggregate, and deliverWithRetry. The cursor must be written transactionally with the normalized records or an equivalent idempotent operation; otherwise a crash between those writes can lose an incident.

Timeouts, retries, and safe webhook delivery

Every outbound API call can hang, fail at the network layer, return a throttling response, or return a non-success status. Bound it with an abort timeout. Classify an AbortError separately from an operational FetchError; record the status code and a safely truncated response body without storing secrets.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Retry only transient conditions, such as connection resets, timeouts, and selected 5xx or rate-limit responses. Use exponential backoff with a cap and a maximum attempt count. Include an idempotency key based on the incident key and alert version. The receiver can then accept a retry without creating a second incident.

async function deliverWithRetry(idempotencyKey, payload) {
  let delay = 1000;
  for (let attempt = 1; attempt <= 5; attempt += 1) {
    try {
      const response = await requestJson(process.env.ALERT_WEBHOOK, {
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          'idempotency-key': idempotencyKey
        },
        body: JSON.stringify(payload)
      });
      const body = (await response.text()).slice(0, 2000);
      await store.recordDelivery({ idempotencyKey, attempt, status: response.status, body });
      if (response.ok) return;
      if (![408, 429, 500, 502, 503, 504].includes(response.status)) break;
    } catch (error) {
      await store.recordDelivery({ idempotencyKey, attempt, error: error.name });
    }
    await new Promise(resolve => setTimeout(resolve, delay));
    delay = Math.min(delay * 2, 30000);
  }
  await store.markDeliveryFailed(idempotencyKey);
  throw new Error(`delivery failed for ${idempotencyKey}`);
}

A failed notification must itself be observable. Export a metric or send the pending item to a durable queue that an operator can replay. Never silently discard it after the final attempt.

Heartbeat checks for jobs that never ran

Send a start heartbeat immediately before the game job begins and a completion heartbeat in a finally path after success or failure. The monitor should know the expected cadence and a grace period. A missing start means the scheduler or process did not launch the job; a missing completion means the job hung, was killed, or failed before reporting.

Include a monotonic run ID, scheduled time, actual start time, completion time, duration, deployment version, and item count. Reject stale heartbeats from an old deployment, and sign requests or authenticate them so an arbitrary client cannot mark a dead job healthy.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

HTTP telemetry is a common implementation for background jobs, websites, and APIs. A hosted monitor can reduce maintenance, but verify its retention, outage behavior, data residency, and program terms before depending on it for escalation.

Use node-cron lifecycle events as signals

node-cron documents execution:failed, execution:missed, execution:overlap, and task:failed events. Subscribe to them and route each event through the same incident pipeline as an API error. Include the event name, task name, scheduled timestamp, run ID when available, and error details.

The library also supports custom info, warn, error, and debug logging. Send structured records to your normal log sink so an operator can correlate scheduler events with API queries and webhook attempts.

For CPU-heavy or long-running work, node-cron background tasks execute in a forked process and support executeTimeout and startTimeout. Forward timeout and child-process failures to the same alert path; otherwise a worker can disappear while the parent scheduler still appears healthy.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Process crashes and deployment safety

Do not try to continue normal operation after an uncaught exception. Node.js states: “It is not safe to resume normal operation after ‘uncaughtException’.” Treat the process as compromised, exit, and let an external supervisor restart it. The supervisor must also generate an alert when the process stops, because no in-process heartbeat can run after a crash.

During deployment, drain or pause the old instance before starting the new one, preserve the durable cursor, and prevent both versions from polling simultaneously. A named distributed lock should have an expiry and owner token so a dead instance cannot hold it forever. Test clock changes and daylight-saving transitions even when production runs in UTC.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Alert payload that an on-call engineer can act on

{
  "type": "game_job_failure",
  "incidentKey": "game-failure:abc123",
  "game": "arena-eu",
  "job": "match-settlement",
  "errorClass": "DatabaseTimeout",
  "firstSeen": "2026-10-18T12:00:04Z",
  "latestSeen": "2026-10-18T12:08:51Z",
  "count": 7,
  "deployment": "2026.10.18-4",
  "runId": "poll-run-id",
  "latestError": "connection timed out after 5000 ms",
  "runbook": "https://ops.example.invalid/runbooks/match-settlement"
}

Redact player identifiers, tokens, and full request bodies. Keep the latest error useful enough to select a runbook, but cap its length and normalize stack traces so deduplication is stable.

Choosing a deployment model

Model Detection coverage Delivery reliability Operational trade-off
Self-built in-process poller Recorded errors plus scheduler events; heartbeat is required for silent stoppage. You control retries, storage, and idempotency. Requires on-call ownership, durable state, locking, and crash supervision.
Forked background task Adds child-process start and execution timeouts. Uses the same delivery path but needs child failure handling. Useful for CPU-heavy work; more process lifecycle complexity.
Hosted HTTP monitor Strong heartbeat and endpoint visibility; still depends on your job sending check-ins. Provider usually supplies persistence and notification retries. Less maintenance, but review provider availability, retention, data residency, and terms.
Framework-integrated monitoring Can capture framework cron errors and apply alert rules. Often offers webhook, chat, or email channels. Convenient for an existing framework, but tied to that deployment and configuration model.

For teams already using NestJS, NestJS Observe documents automatic cron-run error capture and alert rules that can notify through webhooks, Slack, or email. Confirm current capabilities and pricing before adopting it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Testing and troubleshooting checklist

  • Duplicate alerts: replay the same API page and verify that the provider event ID or derived key suppresses duplicates.
  • Lost alerts after restart: kill the process between query and cursor write; confirm the overlap re-reads records and idempotency prevents duplicate delivery.
  • Silent stoppage: stop the scheduler without creating an error record; verify the missing-start or missing-completion heartbeat fires.
  • Slow provider: delay the API beyond the abort timeout and confirm the run is marked failed without advancing the cursor.
  • Webhook outage: return 500, 429, and malformed responses; check capped retries, stored response details, and a visible pending state.
  • Overlapping runs: make one poll last longer than ten minutes and verify noOverlap or the distributed lock blocks a second run.
  • Crash recovery: raise an uncaught exception and ensure the external supervisor restarts the process and alerts on the downtime.
  • Deployment race: run two instances with the same task name and verify only the lock owner polls.
  • Time-zone changes: test the configured IANA timezone around a daylight-saving transition and confirm the intended schedule.

Operational costs and retention decisions

The direct cost of a self-built poller is usually storage, API calls, log volume, and engineering/on-call time rather than a fixed per-alert statistic. Retain cursor and delivery records long enough to investigate missed incidents, but apply a finite retention policy to error bodies and payloads containing sensitive data. A hosted monitor trades engineering effort for vendor availability and data-retention considerations; it does not remove the need for a heartbeat in the game job.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.