How Scheduled Rank Checks Work in OpenSEO: A Deep Dive into the Cron Worker Architecture

OpenSEO performs scheduled rank checks through a dedicated cron worker that identifies due trackers, validates eligibility, atomically claims scheduling slots, and launches Rank Check Workflows while enforcing API budget limits and execution deadlines.

OpenSEO, the open-source SEO platform developed by Every App, automates keyword rank tracking through a sophisticated scheduled job system. Understanding how scheduled rank checks work in OpenSEO helps developers customize tracking intervals, debug scheduling issues, or extend the platform's automation capabilities. This article examines the complete execution flow—from cron trigger to workflow launch—based on the actual implementation in the every-app/open-seo repository.

The Four-Stage Scheduling Pipeline

The scheduled rank check system operates through four distinct stages, each designed to prevent duplicate runs, respect billing plan limits, and maintain reliable execution.

Stage 1: Identify Due Trackers

The cron worker begins by querying for trackers ready to execute. In src/server/features/rank-tracking/services/scheduledRankChecks.ts, the runScheduledRankChecks function calls RankTrackingRepository.getDueConfigsWithOrganization with the current ISO timestamp:

const nowIso = new Date().toISOString();
const dueConfigs = await RankTrackingRepository.getDueConfigsWithOrganization(nowIso);

This query returns all RankTrackingConfig records where nextCheckAt has passed and includes joined organization data for plan validation.

Stage 2: Validate Eligibility

For each due configuration, the scheduler applies three validation filters using isScheduledRankTrackingInterval from src/shared/rank-tracking.ts:

  • Schedule interval verification — confirms daily, weekly, or monthly (rejects manual triggers)
  • Paid plan check — calls customerHasPaidPlan for hosted deployments
  • Keyword presence — skips empty trackers and free-plan configurations

Malformed or ineligible configs are silently skipped without modifying their schedule.

Stage 3: Atomic Slot Claiming

To prevent race conditions between concurrent cron invocations, OpenSEO uses an optimistic locking pattern via RankTrackingRepository.claimDueConfig:

const nextCheckAt = computeNextCheckAt(interval, config.nextCheckAt);
const claimed = await RankTrackingRepository.claimDueConfig({
  configId: config.id,
  projectId: config.projectId,
  observedNextCheckAt: config.nextCheckAt, // used for optimistic check
  nextCheckAt,
  lastSkipReason: null,
});

The observedNextCheckAt parameter ensures the update only succeeds if the row hasn't changed since reading. Upon successful claim:

  • The nextCheckAt timestamp advances using computeNextCheckAt
  • Any previous lastSkipReason is cleared
  • The config becomes ineligible for other workers until the next interval

If claiming fails due to concurrent modification, the config is skipped for this tick.

Stage 4: Workflow Initialization

With the slot secured, beginRankCheckRun from src/server/features/rank-tracking/services/rankCheckRunGuards.ts launches the actual execution:

await beginRankCheckRun({
  workflow: env.RANK_CHECK_WORKFLOW,
  config,
  projectId: config.projectId,
  billingCustomer: {/* system user */},
  keywordsTotal: kwCount,
  trigger: "scheduled",
  workflowStartErrorMessage: "Failed to start scheduled workflow",
});

The trigger: "scheduled" metadata distinguishes automated runs from manual initiations in analytics and billing records.

Runtime Guards: Budget and Deadline Enforcement

OpenSEO implements two protective mechanisms to ensure reliable operation within infrastructure constraints.

Task Unit Budget Cap

The constant SCHEDULED_TASK_UNIT_BUDGET = 1000 limits keyword/device pairs processed per cron tick. With typical configurations requiring approximately 2 API calls per pair, this keeps DataForSEO requests below the 2,000/minute rate limit:

// In runScheduledRankChecks
const remainingBudget = SCHEDULED_TASK_UNIT_BUDGET - usedBudget;
if (keywordDevicesCount > remainingBudget) {
  // Defer remaining configs to next tick
}

Three-Minute Execution Deadline

The TICK_DEADLINE_MS = 180000 timer stops processing if the cron exceeds three minutes, ensuring the Worker doesn't exceed platform limits:

Guard Constant Purpose
Budget SCHEDULED_TASK_UNIT_BUDGET = 1000 Caps DataForSEO API usage per tick
Deadline TICK_DEADLINE_MS = 3 min Prevents Worker timeout violations

Failure Recovery and Observability

When beginRankCheckRun cannot start a workflow (overlapping run, rate limit, or transient error), the scheduler restores the original nextCheckAt rather than advancing the schedule. This guarantees the tracker remains due for immediate retry on the next cron invocation.

Success and failure metrics aggregate into a structured log entry tagged rank_tracking_scheduler_summary, surfacing in Cloudflare Workers logs with counts of:

  • Configs processed
  • Tasks budgeted and started
  • Errors encountered
  • Execution duration

Computing Next Check Times

The computeNextCheckAt utility in src/shared/rank-tracking.ts calculates interval boundaries relative to either:

  • The previous nextCheckAt (for consistent scheduling alignment)
  • Current time (for new or restored trackers)

Supported intervals defined in src/types/schemas/rank-tracking.ts:

export const ScheduledRankTrackingInterval = z.enum([
  "daily",
  "weekly",
  "monthly",
  "manual"
]);

Key Implementation Files

File Responsibility
src/server/features/rank-tracking/services/scheduledRankChecks.ts Cron entry point, budget/deadline enforcement
src/server/features/rank-tracking/services/rankCheckRunGuards.ts Workflow launch and overlap prevention
src/shared/rank-tracking.ts Time calculation and interval validation
src/types/schemas/rank-tracking.ts Zod schemas for type-safe configuration
src/server/workflows/RankCheckWorkflow.ts Actual rank checking execution
src/server/features/rank-tracking/repositories/RankTrackingRepository.ts Database operations for due configs and claims

Summary

  • Scheduled rank checks in OpenSEO execute through a cron worker using four coordinated stages: discovery, validation, atomic claiming, and workflow launch.
  • Optimistic locking via claimDueConfig prevents duplicate runs without database transactions.
  • Budget and deadline guards (1000 task units, 3 minutes) protect against API rate limits and Worker timeouts.
  • Failure recovery preserves original nextCheckAt timestamps when workflows fail to start, ensuring no missed checks.
  • Comprehensive logging through rank_tracking_scheduler_summary enables operational visibility.

Frequently Asked Questions

How does OpenSEO prevent the same rank check from running twice?

OpenSEO uses optimistic concurrency control—each worker attempts to atomically claim a configuration by matching the observedNextCheckAt timestamp. Only one claim succeeds; others skip to the next due config. The nextCheckAt only advances after successful workflow initiation.

What happens if the cron worker exceeds its three-minute deadline?

Processing stops immediately when TICK_DEADLINE_MS elapses. Unprocessed due configs remain in the database with their original nextCheckAt timestamps, making them eligible for the next cron invocation. This prevents Worker timeouts while ensuring no data loss.

Can free-plan organizations use scheduled rank checks?

Scheduled automation (daily, weekly, monthly) requires a paid plan in hosted mode—the customerHasPaidPlan check filters free organizations before workflow launch. Free plans may still use manual trigger intervals initiated by users.

Where is the next check time calculated in OpenSEO?

The computeNextCheckAt function in src/shared/rank-tracking.ts handles all schedule arithmetic, accepting an interval type and optional anchor timestamp to produce ISO-8601 timestamps. It supports interval rollover logic for consistent scheduling alignment.

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 →