# How the Scheduled Rank Check Cron Job Executes Daily Rank Tracking in Open‑SEO

> Discover how Open-SEO's scheduled rank check cron job executes daily rank tracking using Cloudflare Workers. Learn about its efficient reconciliation and dispatch process.

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

---

**Open‑SEO runs its daily rank tracking through a Cloudflare Worker cron trigger that fires every five minutes, invoking a `scheduled` handler in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) which reconciles stale audits and dispatches `runScheduledRankChecks()` to queue due configurations within strict task-unit and time budgets.**

The Open‑SEO platform automates SEO rank tracking through a lightweight, cron-driven scheduler. According to the `every-app/open-seo` source code, the system leverages Cloudflare Workers cron triggers to continuously monitor and launch rank checks without manual intervention.

## Cron Configuration in wrangler.jsonc

The `wrangler.jsonc` file declares two cron schedules:

- `*/5 * * * *` — fires every 5 minutes to drive rank tracking
- `17 3 * * *` — fires once daily at 03:17 UTC for OAuth KV cleanup

The **5-minute cron** is the primary trigger for the scheduled rank check cron job. Each time Cloudflare invokes the Worker with this schedule, the `scheduled` export in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) executes.

## The scheduled Entry Point in src/server.ts

In [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts), the Worker exports a `scheduled` handler that acts as the router for cron invocations. Its logic prioritizes system health before launching new work.

```ts
// src/server.ts – scheduled entrypoint
export default {
  fetch,
  async scheduled(controller, env, _ctx) {
    // OAuth KV cleanup runs on the daily 17 3 cron
    if (controller.cron === "17 3 * * *") { … }

    // First reconcile stale audits, then run rank checks
    await withPgClient(() => reconcileStaleAudits());
    await withPgClient(() => runScheduledRankChecks(env));
  },
};

```

The **watchdog step** (`reconcileStaleAudits`) runs first. This reconciles any audits stuck in a "running" state, ensuring that a slow tick cannot starve the cleanup process. Immediately after, it calls `runScheduledRankChecks(env)` to process due rank-tracking configurations.

## Rank Check Scheduling Logic in scheduledRankChecks.ts

The core scheduling implementation lives in [`src/server/features/rank-tracking/services/scheduledRankChecks.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/scheduledRankChecks.ts). The `runScheduledRankChecks` function performs a batched loop that respects both API limits and wall-clock deadlines.

```ts
// src/server/features/rank-tracking/services/scheduledRankChecks.ts
export async function runScheduledRankChecks(env: Env) {
  const nowIso = new Date().toISOString();
  const dueConfigs = await RankTrackingRepository.getDueConfigsWithOrganization(nowIso);
  const deadline = Date.now() + TICK_DEADLINE_MS;
  let unitsStarted = 0;

  for (const config of dueConfigs) {
    if (Date.now() >= deadline) break;               // wall‑clock guard
    const taskUnits = (keywordCounts.get(config.id) ?? 0) *
                      devicesCount(config.devices);
    if (unitsStarted + taskUnits > SCHEDULED_TASK_UNIT_BUDGET) break; // budget guard

    const claimed = await RankTrackingRepository.claimDueConfig({ … });
    if (!claimed) continue;                         // already running elsewhere

    await beginRankCheckRun({
      workflow: env.RANK_CHECK_WORKFLOW,
      config,
      trigger: "scheduled",
      …
    });
    unitsStarted += taskUnits;
  }

  console.log({
    event: "rank_tracking_scheduler_summary",
    started,
    unitsStarted,
    … // other statistics
  });
}

```

### Fetching Due Configurations

The scheduler begins by querying `RankTrackingRepository.getDueConfigsWithOrganization(nowIso)`. This returns every rank-tracking configuration whose `nextCheckAt` timestamp is in the past, ensuring only eligible projects enter the processing loop.

### Budget and Deadline Enforcement

Two hard guards prevent the job from overwhelming the DataForSEO API or exceeding Cloudflare Worker limits:

- **Task-unit budget**: `SCHEDULED_TASK_UNIT_BUDGET` caps each tick at **1,000 task units**, calculated as the product of keyword count and device count per configuration.
- **Wall-clock deadline**: `TICK_DEADLINE_MS` is set to **3 minutes**. If `Date.now()` reaches the deadline, the loop aborts immediately.

These limits ensure the scheduled rank check cron job runs safely within API request caps and Worker execution constraints.

### Plan Verification and Claiming

For hosted deployments, the scheduler verifies that the organization maintains a paid plan via `customerHasPaidPlan`. Configurations without a valid plan are skipped and tagged with `lastSkipReason: "plan_required"`.

Eligible configurations are then claimed through `RankTrackingRepository.claimDueConfig`. This atomic claim step prevents duplicate work when multiple Worker instances or ticks overlap.

### Workflow Invocation

Once claimed, each configuration is passed to `beginRankCheckRun`, which launches the `RankCheckWorkflow` via `env.RANK_CHECK_WORKFLOW`. The workflow receives the `trigger: "scheduled"` context, distinguishing it from manual or event-driven invocations.

## Safety Guards and Observability

After processing, the scheduler emits a structured log entry with the event name `"rank_tracking_scheduler_summary"`. This record includes:

- Number of runs started
- Total task units consumed
- Skip counts and error counts
- Age of the oldest due configuration

Because the cron fires every 5 minutes, the system continuously processes any configurations that become due. The combination of per-tick budgets, deadline guards, and atomic claiming ensures that the scheduled rank check cron job resumes gracefully on the next tick if interrupted.

## Summary

- Open‑SEO defines its cron schedules in `wrangler.jsonc`, using a **5-minute interval** to drive continuous rank tracking.
- The `scheduled` handler in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) reconciles stale audits before calling `runScheduledRankChecks(env)`.
- `runScheduledRankChecks` in [`src/server/features/rank-tracking/services/scheduledRankChecks.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/scheduledRankChecks.ts) queries due configs, enforces a **1,000-unit task budget**, and respects a **3-minute wall-clock deadline**.
- Configurations are atomically claimed via `RankTrackingRepository.claimDueConfig` to prevent duplicate runs.
- Paid-plan checks filter out ineligible hosted organizations before workflow invocation.
- Structured logging at `"rank_tracking_scheduler_summary"` provides observability into scheduler performance and backlog health.

## Frequently Asked Questions

### How often does Open‑SEO run the scheduled rank check cron job?

The cron job fires every **5 minutes** via the `*/5 * * * *` schedule defined in `wrangler.jsonc`. This sub-daily frequency allows the system to continuously process rank-tracking configurations as they become due, effectively providing daily or more frequent updates depending on the project schedule.

### What happens if a rank check takes longer than one tick to process?

The scheduler enforces a **3-minute wall-clock deadline** (`TICK_DEADLINE_MS`) and a **1,000 task-unit budget** per tick. If limits are reached, the loop exits cleanly and any unprocessed due configs wait for the next 5-minute cron invocation. The atomic `claimDueConfig` step ensures that already-started work is not duplicated.

### Where does the actual SEO rank checking happen?

The scheduler in [`scheduledRankChecks.ts`](https://github.com/every-app/open-seo/blob/main/scheduledRankChecks.ts) does not perform the SEO queries directly. Instead, it invokes `beginRankCheckRun` to start the **`RankCheckWorkflow`** defined in [`src/server/workflows/RankCheckWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/RankCheckWorkflow.ts). This Cloudflare Workers Workflow executes the actual DataForSEO API calls and stores results.

### Why does the scheduled handler reconcile stale audits before rank checks?

The `reconcileStaleAudits` call in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) runs first to recover any audits stuck in a "running" state from previous ticks. This ordering guarantees that a slow or overloaded tick cannot starve the watchdog, maintaining overall system health before new rank-check work is admitted.