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

> Learn how OpenSEO calculates next check times for daily weekly and monthly rank tracking. Discover its interval logic to prevent drift and randomize execution windows. Explore the code.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-09-04

---

**OpenSEO calculates next check times by storing a `nextCheckAt` timestamp for each configuration and updating it via `computeNextCheckAt` in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) contains the calculation logic, while [`src/server/features/rank-tracking/services/scheduledRankChecks.ts`](https://github.com/every-app/open-seo/blob/main/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.