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 ofdaily,weekly,monthly, ormanualnextCheckAt— 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:
- Query due configs — Selects rows where
nextCheckAt ≤ NOW() - Validate billing — Skips if the organization lacks a paid plan
- Handle zero-keyword configs — Skips execution but still advances the schedule to prevent infinite looping
- Advance schedule before execution — Updates
nextCheckAtimmediately usingcomputeNextCheckAt(), then starts the rank-check workflow viabeginRankCheckRun
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:
src/shared/rank-tracking.ts— Core utilities:isScheduledRankTrackingInterval(),computeNextCheckAt(), label helperssrc/types/schemas/rank-tracking.ts— TypeScript definitions forscheduleIntervalandnextCheckAtsrc/server/features/rank-tracking/services/RankTrackingService.ts— Config creation and update logic with schedule computationsrc/server/features/rank-tracking/services/scheduledRankChecks.ts— Cron worker implementation with the advance-then-execute patternsrc/serverFunctions/rank-tracking.ts— Public API endpoints including manual triggerssrc/server/features/rank-tracking/repositories/RankTrackingRepository.ts— Database persistence layer for schedules
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →