MongoDB Aggregation Alerts on a Schedule: Pipelines, Thresholds

Published Jan 28, 2026

Learn how to run MongoDB aggregation pipelines on a schedule and send alerts. Patterns, code examples, performance, and safety tips.

MongoDB Aggregation Alerts on a Schedule: Pipelines, Thresholds

Dashboards are great for exploration, but they’re a poor fit for time-sensitive monitoring. If a metric crosses a threshold at 2:07 a.m.—a spike in failed payments, an abnormal drop in signups, or a sudden surge in error logs—you want an alert, not a chart you’ll notice later. That’s where mongodb aggregation alerts on a schedule becomes a practical pattern: run an aggregation pipeline at fixed intervals, evaluate the result, and notify the right people.

This guide shows how to design safe aggregation-based checks, schedule them reliably, and reduce noise with good alert logic. You’ll also get copy-pastable code snippets (Node.js) and concrete pipeline examples you can adapt.

When aggregation-based alerts beat simple count checks

A basic “count documents and alert if > N” works for a few cases. Aggregation shines when you need one or more of the following:

  • Grouping (e.g., errors by service, region, or endpoint)
  • Time bucketing (e.g., compare last 15 minutes vs previous hour)
  • Joins-like enrichment via $lookup (use carefully)
  • Derived metrics (rates, percentiles, ratios)
  • Outlier detection (e.g., sudden change from baseline)

Core design: pipeline → evaluation → notification

A scheduled aggregation alert has three moving parts:

  1. Query: An aggregation pipeline that returns a small, stable result set (ideally a single document with key metrics).
  2. Evaluation: Logic that decides whether to alert (thresholds, comparisons, anomaly rules, silence windows).
  3. Delivery: Email, Slack, PagerDuty, push notifications—plus deduping so you don’t spam.

Good monitoring is less about collecting data and more about deciding what deserves an interruption.

Example 1: Alert when failed checkouts spike (last 15 minutes)

Assume a collection orders with fields: createdAt, status, amount, and paymentProvider. We’ll compute failures in the last 15 minutes and alert if failures exceed a threshold.

Aggregation pipeline

const pipeline = [
  {
    $match: {
      createdAt: { $gte: new Date(Date.now() - 15 * 60 * 1000) },
      status: { $in: ["PAYMENT_FAILED", "PAYMENT_DECLINED"] }
    }
  },
  {
    $group: {
      _id: "$paymentProvider",
      failures: { $sum: 1 },
      totalAmount: { $sum: "$amount" }
    }
  },
  { $sort: { failures: -1 } },
  { $limit: 5 }
];

Why this pipeline is alert-friendly

  • It uses a bounded time window (last 15 minutes), keeping work predictable.
  • It returns a small result set ($limit), which is ideal for notifications.
  • It groups by a meaningful dimension (provider), making the alert actionable.

Scheduling options (and how to choose)

There are multiple ways to run mongodb aggregation alerts on a schedule. Your choice depends on operational overhead, reliability needs, and where secrets live.

Approach Best for Pros Cons
System cron + script Simple setups Fast to start, cheap Host reliability, secret management, logging
Serverless scheduled function Teams that want minimal ops Auto-scaling, managed runtime Cold starts, vendor-specific setup
CI scheduler (e.g., GitHub Actions cron) Non-critical checks Easy workflows, built-in logs Not ideal for strict SLAs, rate limits
Dedicated monitoring service/app High signal alerts, push delivery Notification UX, scheduling UI Tooling choice, permissions review

Example 2: Rate-based alert (failures / attempts)

Absolute counts can be misleading during traffic changes. A more stable approach is a rate: failures divided by total attempts in the same window.

const pipeline = [
  {
    $match: {
      createdAt: { $gte: new Date(Date.now() - 10 * 60 * 1000) },
      status: { $in: ["PAYMENT_SUCCESS", "PAYMENT_FAILED", "PAYMENT_DECLINED"] }
    }
  },
  {
    $group: {
      _id: null,
      attempts: { $sum: 1 },
      failures: {
        $sum: {
          $cond: [
            { $in: ["$status", ["PAYMENT_FAILED", "PAYMENT_DECLINED"]] },
            1,
            0
          ]
        }
      }
    }
  },
  {
    $project: {
      _id: 0,
      attempts: 1,
      failures: 1,
      failureRate: {
        $cond: [
          { $eq: ["$attempts", 0] },
          0,
          { $divide: ["$failures", "$attempts"] }
        ]
      }
    }
  }
];

Alert example: if attempts ≥ 50 and failureRate ≥ 0.08. This avoids paging on tiny sample sizes.

Implementation: a minimal scheduled runner (Node.js)

Below is a compact pattern you can use in cron, a container, or a serverless job. It runs an aggregation, evaluates the result, and posts a webhook.

import { MongoClient } from "mongodb";
import fetch from "node-fetch";

const uri = process.env.MONGODB_URI; // use TLS, least-privileged user
const webhook = process.env.ALERT_WEBHOOK_URL;

async function runCheck() {
  const client = new MongoClient(uri, { maxPoolSize: 2 });
  await client.connect();

  try {
    const db = client.db(process.env.DB_NAME);
    const orders = db.collection("orders");

    const pipeline = [
      {
        $match: {
          createdAt: { $gte: new Date(Date.now() - 10 * 60 * 1000) },
          status: { $in: ["PAYMENT_SUCCESS", "PAYMENT_FAILED", "PAYMENT_DECLINED"] }
        }
      },
      {
        $group: {
          _id: null,
          attempts: { $sum: 1 },
          failures: {
            $sum: {
              $cond: [
                { $in: ["$status", ["PAYMENT_FAILED", "PAYMENT_DECLINED"]] },
                1,
                0
              ]
            }
          }
        }
      },
      {
        $project: {
          _id: 0,
          attempts: 1,
          failures: 1,
          failureRate: {
            $cond: [
              { $eq: ["$attempts", 0] },
              0,
              { $divide: ["$failures", "$attempts"] }
            ]
          }
        }
      }
    ];

    const [result] = await orders.aggregate(pipeline, { allowDiskUse: false }).toArray();
    const attempts = result?.attempts ?? 0;
    const failureRate = result?.failureRate ?? 0;

    const shouldAlert = attempts >= 50 && failureRate >= 0.08;

    if (shouldAlert) {
      const payload = {
        text: `Payment failures elevated: ${(failureRate * 100).toFixed(1)}% over last 10m (attempts=${attempts}).`
      };
      await fetch(webhook, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload)
      });
    }

    console.log({ attempts, failureRate, shouldAlert });
  } finally {
    await client.close();
  }
}

runCheck().catch((e) => {
  console.error(e);
  process.exit(1);
});

Make alerts reliable: dedupe, hysteresis, and state

The biggest reason scheduled alerts fail is alert noise. Fix it with a small amount of state.

Recommended patterns

  • Dedupe window: Don’t send the same alert more than once every X minutes unless it gets worse.
  • Hysteresis: Use different thresholds to trigger vs recover (e.g., trigger at 8%, recover at 5%).
  • Consecutive failures: Require 2–3 failing intervals before alerting to avoid flukes.
  • Store last status in a small collection (e.g., alert_state) keyed by alert name.

A minimal alert_state document might look like:

{
  "_id": "payment_failure_rate",
  "lastSentAt": "2026-01-01T10:10:00.000Z",
  "lastStatus": "ALERTING",
  "lastValue": 0.091
}

Performance and safety tips for scheduled aggregation

Scheduled pipelines can be safe and low-impact—if you design them that way.

Indexing and selectivity

  • Put a compound index on your primary filter fields. For time-windowed checks, index createdAt (often with a secondary field like status).
  • Keep $match as the first stage whenever possible.
  • Avoid unbounded scans: always include a time range or another selective predicate.

Keep results small

  • Prefer single-metric outputs ($group to _id: null) or top-N outputs ($sort + $limit).
  • Project only what you need with $project.

Avoid heavy joins in hot paths

$lookup can be useful, but scheduled jobs that run frequently can stress the cluster if they join large collections. If you need enrichment, consider denormalizing critical fields or running the check less often.

Use read-only credentials and encrypted connections

  • Create a database user with read-only permissions for the specific DB/collections needed.
  • Require TLS, and keep connection strings in a secret manager (not in code or CI logs).

Testing your alert before trusting it

Before you rely on a scheduled check:

  1. Run the pipeline in the MongoDB shell/Compass with representative data.
  2. Backtest by replaying time windows (e.g., run the same pipeline for historical intervals) to estimate false positives.
  3. Add observability: log the computed metric, threshold, and decision, so you can explain alerts later.

Common pitfalls (and fixes)

  • Time zone confusion: store timestamps in UTC; compute windows in UTC.
  • Empty windows: handle “no data” explicitly (attempts = 0) to avoid divide-by-zero and misleading alerts.
  • Too-frequent schedules: if your pipeline is expensive, reduce frequency or optimize indexes. An alert every minute isn’t useful if it costs you cluster performance.
  • Alert fatigue: add dedupe + hysteresis early; it’s harder to retrofit once people start ignoring alerts.

Putting it together: a practical checklist

  • Define the metric and the decision rule (thresholds, minimum sample size, recovery).
  • Write an aggregation pipeline that is time-bounded and index-friendly.
  • Schedule it with a reliable runner (cron/serverless/CI), and keep secrets safe.
  • Implement dedupe/hysteresis and store small state.
  • Choose the right notification channel for urgency (email for low urgency, paging/push for high).

One last note on tooling

If you prefer running scheduled, read-only database checks with push/email delivery from a mobile-first workflow, an app like Query Alert can fit into that pattern—but the core principles above still apply regardless of tool: design efficient pipelines, evaluate carefully, and notify only when it’s actionable.