# How Rank Tracking Works in OpenSEO with Scheduled SERP Checks

> Discover how OpenSEO rank tracking works with scheduled SERP checks. Automate keyword position monitoring daily, weekly, or monthly to ensure accurate SEO insights.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-08-07

---

**OpenSEO tracks keyword positions by creating a rank-tracking config that runs on manual, daily, weekly, or monthly schedules, using a Cloudflare cron job to trigger SERP checks and advance the next run time to prevent schedule drift.**

Rank tracking in OpenSEO combines configurable scheduling with automated SERP monitoring to track keyword positions across devices and locations. The open-source codebase in `every-app/open-seo` implements a drift-free scheduling system that handles credit validation, workflow orchestration, and result persistence. This article examines the technical implementation of scheduled SERP checks, from config definition to final snapshot storage.

## Understanding the Rank Tracking Configuration

Each rank-tracking session begins with a configuration that binds a domain to specific search parameters. In [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts), the system stores the `scheduleInterval` as a string enum with values `"daily"`, `"weekly"`, `"monthly"`, or `"manual"`.

The helper function `isScheduledRankTrackingInterval` determines whether a config qualifies for automatic execution. This check runs before any scheduling logic to filter out manually triggered configurations.

## Computing the Next Check Time

The scheduling engine relies on `computeNextCheckAt` in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) (lines 21-66) to generate UTC timestamps for future runs. This function handles three distinct interval types while preventing schedule drift from delayed executions.

### Handling Monthly, Daily, and Weekly Intervals

For **monthly** configurations, the algorithm calculates the end of the next month and selects a random hour between 04:00 and 09:00 UTC to distribute load. **Daily** configs add exactly 24 hours to the previous anchor time, while **weekly** configs advance by 7 days. Both daily and weekly intervals also inject random hours and minutes to prevent thundering herd problems.

### Preventing Schedule Drift

If a previous `nextCheckAt` timestamp is provided, the function advances from that anchor point until the result lies in the future. This ensures that delayed runs—caused by downtime or credit shortages—do not create a permanent offset in the schedule. The system catches up to real-time before calculating the next valid slot.

## The Scheduled Check Cron Job

Cloudflare Workers execute `runScheduledRankChecks` every minute from [`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) (lines 10-93). This cron handler serves as the entry point for all automated rank tracking.

### Finding Due Configurations

The worker queries `RankTrackingRepository.getDueConfigsWithOrganization` to fetch configurations where `nextCheckAt` is less than or equal to the current timestamp. For each due config, the system performs two validation checks:

- Skips the config if the organization lacks a paid plan in hosted mode
- Skips and advances the schedule if the config contains no keywords

### Workflow Initialization

For valid configs, the cron immediately updates `nextCheckAt` using `computeNextCheckAt` before starting the workflow. This early advancement prevents retry storms if the subsequent workflow fails. The system then invokes `beginRankCheckRun` to initiate the [`RankCheckWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/RankCheckWorkflow.ts) orchestration.

## Executing the Rank Check Workflow

The workflow defined in [`src/server/workflows/RankCheckWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/RankCheckWorkflow.ts) (lines 25-86) manages the actual SERP retrieval and data persistence.

### Credit Estimation and Validation

Before executing checks, the workflow calls `estimateRankCheckCredits` from [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) (lines 65-76). This function multiplies keyword count by device count and SERP depth cost, applies a markup, and converts USD to internal credits. Scheduled checks always use the cheaper queued pricing model rather than live check rates.

The workflow verifies the organization's credit balance in hosted mode, halting execution if insufficient funds exist.

### SERP Execution and Result Persistence

The system executes SERP requests via `runLiveCheck` or `runQueuedCheck` depending on configuration. After each batch completes, the workflow writes position snapshots to the database. Upon completion, `finalizeRankCheckRun` marks the run as `completed`, records total keywords checked, and updates the config's `lastCheckedAt` timestamp. Notably, the schedule advancement occurred earlier in the cron handler, ensuring the next check time remains independent of workflow duration.

## Code Examples

The following examples demonstrate creating configurations and triggering checks using the server functions defined in [`src/serverFunctions/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/rank-tracking.ts):

```typescript
// 1️⃣ Create a rank-tracking config (client side)
await createRankTrackingConfig({
  domain: "example.com",
  locationCode: 2840,          // US / New York
  languageCode: "en",
  devices: "both",
  serpDepth: 30,               // 3 pages (10 results each)
  scheduleInterval: "weekly", // auto-run weekly
});

// 2️⃣ Manually trigger a check (e.g. after adding keywords)
await triggerRankCheck({
  configId: "<config-id>",
  keywordIds: ["kw1", "kw2"], // optional – limit to specific keywords
});

// 3️⃣ How the cron advances the schedule (internal)
await runScheduledRankChecks(env); // called by the Cloudflare worker every minute

```

## Summary

- **Rank tracking configs** store schedule intervals in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) with options for daily, weekly, monthly, or manual execution.
- **Drift-free scheduling** uses `computeNextCheckAt` to anchor future runs from the previous check time, with random UTC hours between 04:00-09:00 for monthly checks.
- **Cloudflare cron workers** execute `runScheduledRankChecks` every minute to find due configs, validate credits, and advance schedules before starting workflows.
- **Credit estimation** occurs in `estimateRankCheckCredits`, applying queued pricing and USD-to-credit conversion rules from [`src/shared/billing.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/billing.ts).
- **Workflow finalization** in [`RankCheckWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/RankCheckWorkflow.ts) persists snapshots and updates `lastCheckedAt` without modifying the already-advanced next check time.

## Frequently Asked Questions

### How does OpenSEO prevent duplicate scheduled checks?

The system prevents duplicates by immediately updating `nextCheckAt` in the cron handler before invoking the workflow. According to the source code 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), this atomic advancement ensures that even if the workflow fails or retries, the config will not appear in subsequent `getDueConfigsWithOrganization` queries until the newly calculated time arrives.

### What happens if an organization runs out of credits during a scheduled check?

If credit validation fails during [`RankCheckWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/RankCheckWorkflow.ts) execution, the workflow halts before consuming additional resources. In hosted mode, the `estimateRankCheckCredits` function checks balances upfront, and the cron skips configs for unpaid organizations entirely. The schedule still advances normally, so the next check occurs at the proper interval once credits are replenished.

### Can I schedule rank checks for specific times of day?

No, OpenSEO does not support user-defined check times. The `computeNextCheckAt` function in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) automatically assigns random hours between 04:00 and 09:00 UTC for monthly checks, and random hours/minutes for daily and weekly intervals. This distributes server load and prevents thundering herds across the Cloudflare Worker infrastructure.

### Where is the scheduling logic defined in the codebase?

The core scheduling logic resides in two primary locations. [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) contains `computeNextCheckAt` and `isScheduledRankTrackingInterval` for date calculations and interval validation. The cron execution logic 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), which queries due configs and initializes the workflow defined in [`src/server/workflows/RankCheckWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/RankCheckWorkflow.ts).