How OpenSEO Calculates the Next Check Times for Scheduled Rank Tracking (Daily, Weekly, Monthly)

OpenSEO calculates next check times by storing a nextCheckAt timestamp for each configuration and updating it via computeNextCheckAt in src/shared/rank-tracking.ts, which uses interval-specific logic to prevent drift and randomize execution windows between 04:00-09:00 UTC.

OpenSEO implements a robust scheduling system to ensure rank-tracking jobs run at daily, weekly, or monthly intervals without drift. The system stores a next-check timestamp (nextCheckAt) for every scheduled configuration and recalculates it using the computeNextCheckAt utility according to the every-app/open-seo source code. This approach accounts for delayed executions while distributing load across a randomized UTC window.

Core Scheduling Architecture

The scheduler entry point resides in src/server/features/rank-tracking/services/scheduledRankChecks.ts. When runScheduledRankChecks executes (lines 106-108), it identifies due configurations and invokes computeNextCheckAt from src/shared/rank-tracking.ts to determine the next execution timestamp.

The computeNextCheckAt Function

The computeNextCheckAt function (lines 78-123 in src/shared/rank-tracking.ts) serves as the central calculation engine. It accepts the interval type ("daily", "weekly", or "monthly") and an optional previous anchor timestamp to prevent schedule drift.

Monthly Rank Tracking Calculation

Monthly scheduling uses anchor-based logic to maintain consistent execution times across month boundaries.

Existing Anchor Present

When a previous nextCheckAt exists, the function treats it as an anchor (lines 85-94). It repeatedly adds one month using endOfMonthWithTime until the resulting date is strictly in the future (lines 88-93). This prevents drift when runs are delayed by days or weeks.

First-Time Monthly Setup

If no anchor exists, the system selects a random UTC hour between 04 and 09 and a random minute, then applies this time to the last day of the current month (lines 95-104). If that moment has already passed, the same hour and minute transfer to the following month.

Daily and Weekly Interval Logic

Daily and weekly tracking use a unified approach based on the daysAhead parameter.

Calculating Days Ahead

The function sets daysAhead to 1 for daily intervals and 7 for weekly intervals (lines 8-11). This value determines the base increment when calculating future dates.

Handling Delayed Executions

With an existing anchor (previousNextCheckAt), the function computes how many full intervals have elapsed (steps) and adds that many daysAhead to the anchor (lines 10-15). This guarantees the new timestamp advances to the next logical period rather than simply adding one interval to the current time.

Initial Scheduling Without Anchor

Without an anchor, the function adds daysAhead to the current date, then injects a random hour between 04-09 UTC and a random minute (lines 16-22). This creates a future timestamp while spreading system load across the early morning UTC window.

The endOfMonthWithTime Helper

The endOfMonthWithTime function (lines 51-65 in src/shared/rank-tracking.ts) constructs a Date object representing the last day of the target month while preserving the exact time-of-day from the anchor. This ensures monthly checks run at the same hour and minute regardless of month length variations.

Practical Implementation Examples

// Import the utility
import { computeNextCheckAt } from "@/shared/rank-tracking";

// Daily – first run (no previous anchor)
const dailyNext = computeNextCheckAt("daily");
console.log(dailyNext); // e.g. "2026-02-01T06:23:00.000Z"

// Weekly – after a delayed run
const weeklyNext = computeNextCheckAt(
  "weekly",
  "2026-01-31T05:30:00.000Z"   // previous next_check_at (now in the past)
);
console.log(weeklyNext); // advances to the next Monday at the same time window

// Monthly – advancing multiple months if needed
const monthlyNext = computeNextCheckAt(
  "monthly",
  "2025-12-31T07:15:00.000Z"
);
console.log(monthlyNext); // "2026-01-31T07:15:00.000Z" (or later if still past)

Summary

  • Anchor-based calculation prevents schedule drift by counting elapsed intervals rather than adding fixed offsets to the current time.
  • Monthly logic uses endOfMonthWithTime to maintain consistent execution times on the last day of each month.
  • Daily and weekly intervals utilize daysAhead values of 1 and 7 respectively, with step calculations for delayed runs.
  • Randomized UTC window (04:00-09:00) distributes load and prevents thundering herds.
  • Core files: src/shared/rank-tracking.ts contains the calculation logic, while src/server/features/rank-tracking/services/scheduledRankChecks.ts orchestrates the scheduler.

Frequently Asked Questions

How does OpenSEO prevent monthly schedule drift if a job runs late?

OpenSEO prevents drift by using the previous nextCheckAt as an anchor and repeatedly adding months until the resulting timestamp is strictly in the future. As implemented in computeNextCheckAt (lines 88-93), this ensures that even if a job runs days late, the next check advances to the correct future month rather than scheduling it one month from the delayed execution time.

Why does OpenSEO use a random UTC hour between 04:00 and 09:00 for scheduling?

The random hour and minute selection spreads execution load across a five-hour window to prevent thundering herd problems when multiple rank-tracking jobs initialize simultaneously. This load-balancing approach appears in the initialization logic for all three intervals (daily, weekly, and monthly) within computeNextCheckAt.

What happens when a monthly scheduled check falls on a month with fewer days?

The endOfMonthWithTime helper (lines 51-65) constructs the date using the last day of the target month while preserving the original time-of-day from the anchor. This handles month-length variations automatically, ensuring February 28th/29th, April 30th, and other month-end dates resolve correctly without manual adjustment.

Can the daily and weekly intervals handle multi-day delays?

Yes. When an anchor exists, computeNextCheckAt calculates the number of full intervals elapsed (steps) and adds that many days (1 for daily, 7 for weekly) to the anchor timestamp (lines 10-15). This ensures that if a weekly job scheduled for Monday runs on Wednesday, the next check correctly advances to the following Monday rather than scheduling for Wednesday next week.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →