# How OpenSEO's Scheduled Rank Check Cron Job Prevents Schedule Drift

> Discover how OpenSEO's scheduled rank check cron job prevents schedule drift by advancing timestamps after successful workflow completion and calculating future runs from a fixed anchor point.

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

---

**OpenSEO prevents schedule drift by only advancing the `next_check_at` timestamp after successfully claiming and starting a workflow, while the `computeNextCheckAt` function calculates future run times from a fixed anchor point rather than the current execution time.**

The scheduled rank check cron job in [every-app/open-seo](https://github.com/every-app/open-seo) processes keyword ranking checks every five minutes while maintaining strict resource budgets and atomic state management. This architecture ensures that daily, weekly, and monthly rank-tracking configurations execute at consistent intervals even when individual runs fail or experience delays.

## How the Scheduled Rank Check Cron Job Executes

The cron job operates on a 5-minute tick cycle orchestrated through the main server entry point and the `runScheduledRankChecks` service.

### Entry Point and Invocation

In [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts), the cron handler invokes `runScheduledRankChecks` to begin each scheduling cycle. This serves as the primary coordinator for all scheduled rank-check activities.

```typescript
// Cron entry point (src/server.ts)
await withPgClient(() => runScheduledRankChecks(env));

```

### Identifying Due Configurations

The scheduler queries the `RankTrackingRepository` to fetch configurations where `next_check_at` is less than or equal to the current UTC time. The `getDueConfigsWithOrganization` method retrieves these candidates along with their organization data for plan verification.

```typescript
// Fetching due configurations (scheduledRankChecks.ts L40-L42)
const dueConfigs = await rankTrackingRepository.getDueConfigsWithOrganization();

```

### Resource Budgeting and Time Constraints

To prevent system overload, the implementation enforces two hard limits within the processing loop:

- **Task Unit Budget**: A maximum of **1,000 task units** (calculated as keywords × devices) per tick via `SCHEDULED_TASK_UNIT_BUDGET`
- **Wall-Clock Deadline**: A hard stop at **3 minutes** (`TICK_DEADLINE_MS`) ensures completion before the next 5-minute tick

```typescript
// Budget and deadline enforcement
if (started > 0 && unitsStarted + taskUnits > SCHEDULED_TASK_UNIT_BUDGET) {
  stoppedByBudget = true;
  break;
}
if (Date.now() >= deadline) {
  stoppedByDeadline = true;
  break;
}

```

### Plan Verification and Atomic Claiming

For SaaS deployments, the job verifies that the organization maintains a valid paid plan using `customerHasPaidPlan`. If this check fails, the error is logged but **the schedule is not advanced**, allowing the configuration to be retried on subsequent ticks without drift.

Before launching a workflow, the system atomically claims the configuration slot using `claimDueConfig`. This prevents race conditions in multi-worker deployments by ensuring only one instance processes a given rank-tracking configuration.

### Handling Concurrent Executions

If the workflow is already running when the cron fires, the scheduler executes a **schedule restoration** operation. It swaps `observedNextCheckAt` and `nextCheckAt` values to revert the schedule, ensuring the configuration remains due for the next tick rather than being skipped or delayed prematurely.

```typescript
// Schedule restoration when workflow already running (scheduledRankChecks.ts L101-L112)
// Reverts the schedule so the config is retried on next tick without advancing

```

## Drift Prevention Architecture

Schedule drift—where recurring jobs gradually shift later due to processing delays—is eliminated through anchor-based calculation and conservative state advancement.

### The computeNextCheckAt Algorithm

Located in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts), the `computeNextCheckAt` function calculates the next execution time using the **previous `next_check_at` value as a fixed anchor** rather than the current timestamp. This ensures that if a weekly job runs late (for example, on Wednesday instead of Monday), the subsequent check is still scheduled for the following Monday, not the next Wednesday.

```typescript
// computeNextCheckAt guarantees no drift (src/shared/rank-tracking.ts L78-L115)
if (previousNextCheckAt) {
  const anchor = new Date(previousNextCheckAt).getTime();
  const intervalMs = daysAhead * 86_400_000;
  const steps = Math.floor(Math.max(0, now - anchor) / intervalMs) + 1;
  return new Date(anchor + steps * intervalMs).toISOString();
}

```

For initial runs without a previous anchor, the system selects a random hour between **04:00 and 09:00 UTC** with a random minute to distribute load, then applies the appropriate day interval (1 day for daily, 7 days for weekly, or end-of-month for monthly).

### Conservative Schedule Advancement

The schedule is **only advanced after successful workflow initiation**. Transient failures—such as database concurrency conflicts during `claimDueConfig`, plan verification errors, or workflow start failures—leave the original `next_check_at` timestamp unchanged. This guarantees that the next 5-minute tick will retry the same configuration without pushing the schedule forward, preventing permanent drift.

## Summary

- The cron job runs every 5 minutes via `runScheduledRankChecks` in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts), processing rank-tracking configurations through atomic database operations in `RankTrackingRepository`.
- Hard limits of **1,000 task units** and **3-minute execution deadlines** protect system resources from runaway scheduling loops.
- **Drift prevention** relies on `computeNextCheckAt` in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts), which calculates future dates from a fixed anchor point rather than the current execution time.
- The schedule only advances after successful workflow claims and plan verification; failures preserve the original `next_check_at` for retry on the next tick.
- Concurrent execution handling via schedule restoration ensures configurations are never skipped due to existing workflow runs.

## Frequently Asked Questions

### How often does the rank check cron job run in OpenSEO?

The cron job executes every 5 minutes, invoking the `runScheduledRankChecks` function from [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) to evaluate and process due rank-tracking configurations.

### What prevents the scheduled rank check cron job from overloading the system?

OpenSEO implements a dual-throttle mechanism: a **task unit budget** of 1,000 units (keywords × devices) per tick via `SCHEDULED_TASK_UNIT_BUDGET`, and a **3-minute wall-clock deadline** (`TICK_DEADLINE_MS`). The processing loop terminates immediately when either threshold is reached, deferring remaining work to subsequent ticks without schedule advancement.

### What happens if a rank check workflow is already running when the cron fires?

If the system detects an already-running workflow during the claiming phase, it **restores the schedule** by reverting the `next_check_at` value using the `observedNextCheckAt` field. This ensures the configuration remains eligible for processing on the next 5-minute tick without losing its chronological position in the schedule sequence.

### How does OpenSEO handle missed or delayed rank checks without causing schedule drift?

The `computeNextCheckAt` function uses the previous scheduled time as an immutable anchor, adding interval increments (1 day, 7 days, or monthly) until it reaches a future timestamp. Because calculations start from the original anchor rather than the delayed execution time, even significantly late runs do not shift the subsequent schedule, eliminating cumulative drift.