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, ormonthly(rejectsmanualtriggers) - Paid plan check — calls
customerHasPaidPlanfor 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
nextCheckAttimestamp advances usingcomputeNextCheckAt - Any previous
lastSkipReasonis 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
claimDueConfigprevents 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
nextCheckAttimestamps when workflows fail to start, ensuring no missed checks. - Comprehensive logging through
rank_tracking_scheduler_summaryenables 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →