# How OpenSEO Manages Rank Tracking Schedules: Architecture and Implementation Guide

> Discover how OpenSEO manages rank tracking schedules with its timestamp system daily cron worker and shared utilities. Learn about the architecture and implementation.

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

---

**OpenSEO manages rank tracking schedules through a deterministic timestamp-based system stored per configuration, driven by shared utilities and orchestrated via a daily cron worker that advances schedules before execution to prevent retry storms.**

This guide explains how the every-app/open-seo repository implements automated rank tracking schedules. Whether you're configuring daily SEO monitoring or building scheduling logic into your own platform, understanding these mechanics helps you optimize check timing and avoid common pitfalls like schedule drift.

## Core Schedule Concepts in OpenSEO

OpenSEO stores two critical fields on every rank tracking configuration:

- **`scheduleInterval`** — One of `daily`, `weekly`, `monthly`, or `manual`
- **`nextCheckAt`** — The UTC timestamp when the next automatic check should run

These fields live in the database schema defined at [`src/types/schemas/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/types/schemas/rank-tracking.ts) and persist to the `schedule_interval` column. The system intentionally randomizes execution times within a 04:00–09:00 UTC window to distribute load and avoid thundering herd problems against DataForSEO's API.

## Detecting Scheduled vs. Manual Configurations

Before calculating any timestamps, OpenSEO determines whether a configuration should auto-run. The `isScheduledRankTrackingInterval()` utility in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) (lines 83-92) filters out the `"manual"` option:

```typescript
// Returns false for "manual", true for daily/weekly/monthly
const shouldAutoRun = isScheduledRankTrackingInterval(config.scheduleInterval);

```

This simple guard ensures manual configurations never appear in cron queries, while scheduled ones proceed through the timing logic.

## Computing the Next Check Timestamp

The `computeNextCheckAt()` function in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) (lines 121-166) generates deterministic yet randomized future timestamps. Its behavior varies by interval:

### Monthly Scheduling

For monthly checks, the function uses `endOfMonthWithTime()` and selects a random hour between 04:00–09:00 UTC. If the computed time has already passed, it advances to the following month:

```typescript
// Pseudo-implementation showing monthly logic
const nextMonthly = computeNextCheckAt("monthly", "2024-01-15T10:00:00Z");
// → "2024-01-31T04-09-random-UTC" (or February if January 31st passed)

```

### Daily and Weekly Scheduling

Daily intervals add 1 day; weekly intervals add 7 days. Both apply the same 04:00–09:00 UTC randomization:

```typescript
// Daily: advances exactly 24 hours with randomized hour
const nextDaily = computeNextCheckAt("daily", "2024-08-09T07:00:00Z");
// → "2024-08-10T04-09-random-UTC"

// Weekly: advances exactly 7 days with randomized hour
const nextWeekly = computeNextCheckAt("weekly", "2024-08-09T07:00:00Z");
// → "2024-08-16T04-09-random-UTC"

```

### Preventing Schedule Drift

A critical design feature: when a previous `nextCheckAt` anchor exists, `computeNextCheckAt()` steps forward from that anchor until the result exceeds the current time. This prevents schedule drift when runs are delayed. A config due Monday but checked Tuesday still advances to the following Monday, not Wednesday.

## Creating and Updating Configurations

The `RankTrackingService` class in [`src/server/features/rank-tracking/services/RankTrackingService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/RankTrackingService.ts) handles schedule initialization and recomputation.

### Creating a Scheduled Config

Lines 48-55 demonstrate how `createConfig()` automatically computes the first `nextCheckAt`:

```typescript
// Create a new rank-tracking config that checks weekly
await RankTrackingService.createConfig({
  projectId,
  projectMarket,
  domain: "example.com",
  locationCode: 123,
  languageCode: "en",
  devices: "both",
  serpDepth: 3,
  scheduleInterval: "weekly", // daily | weekly | monthly | manual
});

```

The service calls `computeNextCheckAt("weekly")` internally and persists the result.

### Switching to Manual

Lines 49-56 show `updateConfig()` handling schedule changes. When switching to `manual`, `nextCheckAt` becomes `null`:

```typescript
// Change an existing config to manual (no auto schedule)
await RankTrackingService.updateConfig(configId, projectId, {
  scheduleInterval: "manual", // disables auto-run
});

```

Since `isScheduledRankTrackingInterval("manual")` returns false, the system clears the timestamp and excludes this config from future cron queries.

## The Cron Worker: Executing Scheduled Checks

The `runScheduledRankChecks` function 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) (lines 13-61) implements the orchestration layer. It executes four precise steps for each due configuration:

1. **Query due configs** — Selects rows where `nextCheckAt ≤ NOW()`
2. **Validate billing** — Skips if the organization lacks a paid plan
3. **Handle zero-keyword configs** — Skips execution but still advances the schedule to prevent infinite looping
4. **Advance schedule before execution** — Updates `nextCheckAt` immediately using `computeNextCheckAt()`, then starts the rank-check workflow via `beginRankCheckRun`

This **advance-then-execute** pattern is crucial. If the workflow fails later, the schedule has already moved forward, preventing retry storms where the same failing config gets retried indefinitely.

## Manual Triggers: Bypassing the Schedule

Users can force immediate checks regardless of `nextCheckAt`. The `triggerRankCheck` function in [`src/serverFunctions/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/rank-tracking.ts) uses the live DataForSEO API path directly:

```typescript
// Manually trigger a rank check (ignores schedule)
await triggerRankCheck({
  configId,
  projectId,
  billingCustomer,
});

```

Manual triggers do not modify `nextCheckAt`. The existing schedule continues unchanged, and the config simply runs one additional out-of-cycle check.

## Key Implementation Files

Understanding the rank tracking schedule architecture requires familiarity with these source locations:

- **[`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts)** — Core utilities: `isScheduledRankTrackingInterval()`, `computeNextCheckAt()`, label helpers
- **[`src/types/schemas/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/types/schemas/rank-tracking.ts)** — TypeScript definitions for `scheduleInterval` and `nextCheckAt`
- **[`src/server/features/rank-tracking/services/RankTrackingService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/RankTrackingService.ts)** — Config creation and update logic with schedule computation
- **[`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)** — Cron worker implementation with the advance-then-execute pattern
- **[`src/serverFunctions/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/rank-tracking.ts)** — Public API endpoints including manual triggers
- **[`src/server/features/rank-tracking/repositories/RankTrackingRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/repositories/RankTrackingRepository.ts)** — Database persistence layer for schedules

## Summary

OpenSEO's rank tracking schedule system combines several robust design patterns:

- **Interval-based configuration** with four options: `daily`, `weekly`, `monthly`, `manual`
- **Randomized UTC timestamps** within 04:00–09:00 to distribute API load
- **Anchor-based calculation** that prevents schedule drift from delayed runs
- **Advance-then-execute semantics** in the cron worker to eliminate retry storms
- **Clean separation** between automatic scheduling and manual triggers

These mechanisms ensure reliable, predictable SEO monitoring without overwhelming external APIs or creating cascading failure modes.

## Frequently Asked Questions

### How does OpenSEO prevent the same rank check from running multiple times?

The `runScheduledRankChecks` worker updates `nextCheckAt` immediately before launching the workflow. Even if the rank check fails or times out, the timestamp has already advanced, so subsequent cron cycles skip that config until the new future time arrives. This idempotent pattern prevents duplicate executions and retry storms.

### What happens if I change a weekly config to daily mid-cycle?

When you call `RankTrackingService.updateConfig()` with a new `scheduleInterval`, the service recomputes `nextCheckAt` from the current moment using `computeNextCheckAt()`. The old schedule is discarded, and the new interval takes effect immediately. No partial or hybrid scheduling occurs.

### Why does OpenSEO randomize check times instead of using fixed schedules?

Randomization within the 04:00–09:00 UTC window prevents thousands of configs from simultaneously hitting DataForSEO's API at the top of each hour. This protects both OpenSEO's infrastructure and their API provider from thundering herd problems, improving reliability for all users.

### Can I schedule rank checks at a specific hour instead of the random window?

Currently, OpenSEO does not expose hour-level scheduling precision. The `computeNextCheckAt()` implementation hardcodes the 04:00–09:00 randomization for all intervals. To request custom timing, you would need to modify [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) or implement a wrapper that calculates `nextCheckAt` externally and bypasses the standard service methods.