How OpenSEO Manages Rank Tracking Schedules: Architecture and Implementation Guide

OpenSEO manages rank tracking schedules through a deterministic timestamp-based system stored per configuration, driven by shared utilities and orchestrated via a daily cron worker that advances schedules before execution to prevent retry storms.

This guide explains how the every-app/open-seo repository implements automated rank tracking schedules. Whether you're configuring daily SEO monitoring or building scheduling logic into your own platform, understanding these mechanics helps you optimize check timing and avoid common pitfalls like schedule drift.

Core Schedule Concepts in OpenSEO

OpenSEO stores two critical fields on every rank tracking configuration:

  • scheduleInterval — One of daily, weekly, monthly, or manual
  • nextCheckAt — The UTC timestamp when the next automatic check should run

These fields live in the database schema defined at src/types/schemas/rank-tracking.ts and persist to the schedule_interval column. The system intentionally randomizes execution times within a 04:00–09:00 UTC window to distribute load and avoid thundering herd problems against DataForSEO's API.

Detecting Scheduled vs. Manual Configurations

Before calculating any timestamps, OpenSEO determines whether a configuration should auto-run. The isScheduledRankTrackingInterval() utility in src/shared/rank-tracking.ts (lines 83-92) filters out the "manual" option:

// Returns false for "manual", true for daily/weekly/monthly
const shouldAutoRun = isScheduledRankTrackingInterval(config.scheduleInterval);

This simple guard ensures manual configurations never appear in cron queries, while scheduled ones proceed through the timing logic.

Computing the Next Check Timestamp

The computeNextCheckAt() function in src/shared/rank-tracking.ts (lines 121-166) generates deterministic yet randomized future timestamps. Its behavior varies by interval:

Monthly Scheduling

For monthly checks, the function uses endOfMonthWithTime() and selects a random hour between 04:00–09:00 UTC. If the computed time has already passed, it advances to the following month:

// Pseudo-implementation showing monthly logic
const nextMonthly = computeNextCheckAt("monthly", "2024-01-15T10:00:00Z");
// → "2024-01-31T04-09-random-UTC" (or February if January 31st passed)

Daily and Weekly Scheduling

Daily intervals add 1 day; weekly intervals add 7 days. Both apply the same 04:00–09:00 UTC randomization:

// Daily: advances exactly 24 hours with randomized hour
const nextDaily = computeNextCheckAt("daily", "2024-08-09T07:00:00Z");
// → "2024-08-10T04-09-random-UTC"

// Weekly: advances exactly 7 days with randomized hour
const nextWeekly = computeNextCheckAt("weekly", "2024-08-09T07:00:00Z");
// → "2024-08-16T04-09-random-UTC"

Preventing Schedule Drift

A critical design feature: when a previous nextCheckAt anchor exists, computeNextCheckAt() steps forward from that anchor until the result exceeds the current time. This prevents schedule drift when runs are delayed. A config due Monday but checked Tuesday still advances to the following Monday, not Wednesday.

Creating and Updating Configurations

The RankTrackingService class in src/server/features/rank-tracking/services/RankTrackingService.ts handles schedule initialization and recomputation.

Creating a Scheduled Config

Lines 48-55 demonstrate how createConfig() automatically computes the first nextCheckAt:

// Create a new rank-tracking config that checks weekly
await RankTrackingService.createConfig({
  projectId,
  projectMarket,
  domain: "example.com",
  locationCode: 123,
  languageCode: "en",
  devices: "both",
  serpDepth: 3,
  scheduleInterval: "weekly", // daily | weekly | monthly | manual
});

The service calls computeNextCheckAt("weekly") internally and persists the result.

Switching to Manual

Lines 49-56 show updateConfig() handling schedule changes. When switching to manual, nextCheckAt becomes null:

// Change an existing config to manual (no auto schedule)
await RankTrackingService.updateConfig(configId, projectId, {
  scheduleInterval: "manual", // disables auto-run
});

Since isScheduledRankTrackingInterval("manual") returns false, the system clears the timestamp and excludes this config from future cron queries.

The Cron Worker: Executing Scheduled Checks

The runScheduledRankChecks function in src/server/features/rank-tracking/services/scheduledRankChecks.ts (lines 13-61) implements the orchestration layer. It executes four precise steps for each due configuration:

  1. Query due configs — Selects rows where nextCheckAt ≤ NOW()
  2. Validate billing — Skips if the organization lacks a paid plan
  3. Handle zero-keyword configs — Skips execution but still advances the schedule to prevent infinite looping
  4. Advance schedule before execution — Updates nextCheckAt immediately using computeNextCheckAt(), then starts the rank-check workflow via beginRankCheckRun

This advance-then-execute pattern is crucial. If the workflow fails later, the schedule has already moved forward, preventing retry storms where the same failing config gets retried indefinitely.

Manual Triggers: Bypassing the Schedule

Users can force immediate checks regardless of nextCheckAt. The triggerRankCheck function in src/serverFunctions/rank-tracking.ts uses the live DataForSEO API path directly:

// Manually trigger a rank check (ignores schedule)
await triggerRankCheck({
  configId,
  projectId,
  billingCustomer,
});

Manual triggers do not modify nextCheckAt. The existing schedule continues unchanged, and the config simply runs one additional out-of-cycle check.

Key Implementation Files

Understanding the rank tracking schedule architecture requires familiarity with these source locations:

Summary

OpenSEO's rank tracking schedule system combines several robust design patterns:

  • Interval-based configuration with four options: daily, weekly, monthly, manual
  • Randomized UTC timestamps within 04:00–09:00 to distribute API load
  • Anchor-based calculation that prevents schedule drift from delayed runs
  • Advance-then-execute semantics in the cron worker to eliminate retry storms
  • Clean separation between automatic scheduling and manual triggers

These mechanisms ensure reliable, predictable SEO monitoring without overwhelming external APIs or creating cascading failure modes.

Frequently Asked Questions

How does OpenSEO prevent the same rank check from running multiple times?

The runScheduledRankChecks worker updates nextCheckAt immediately before launching the workflow. Even if the rank check fails or times out, the timestamp has already advanced, so subsequent cron cycles skip that config until the new future time arrives. This idempotent pattern prevents duplicate executions and retry storms.

What happens if I change a weekly config to daily mid-cycle?

When you call RankTrackingService.updateConfig() with a new scheduleInterval, the service recomputes nextCheckAt from the current moment using computeNextCheckAt(). The old schedule is discarded, and the new interval takes effect immediately. No partial or hybrid scheduling occurs.

Why does OpenSEO randomize check times instead of using fixed schedules?

Randomization within the 04:00–09:00 UTC window prevents thousands of configs from simultaneously hitting DataForSEO's API at the top of each hour. This protects both OpenSEO's infrastructure and their API provider from thundering herd problems, improving reliability for all users.

Can I schedule rank checks at a specific hour instead of the random window?

Currently, OpenSEO does not expose hour-level scheduling precision. The computeNextCheckAt() implementation hardcodes the 04:00–09:00 randomization for all intervals. To request custom timing, you would need to modify src/shared/rank-tracking.ts or implement a wrapper that calculates nextCheckAt externally and bypasses the standard service methods.

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 →