# OpenSEO Cron Jobs: Complete Guide to Scheduled Tasks and Their Purposes

> Discover OpenSEO's cron jobs: OAuth cleanup, rank tracking, audit reconciliation, and polling safeguards. Learn how these scheduled tasks keep your SEO efficient. Explore the complete guide.

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

---

**OpenSEO runs four scheduled cron jobs that handle OAuth token cleanup, rank tracking, audit reconciliation, and polling safeguards, all dispatched through Cloudflare Workers via `wrangler.jsonc` configuration.**

This guide breaks down every scheduled task in the [every-app/open-seo](https://github.com/every-app/open-seo) repository, including exact cron expressions, source file locations, and how the server dispatches each job. Whether you're debugging stuck audits or extending the scheduler, you'll find the precise implementation details here.

## What Are OpenSEO's Scheduled Cron Jobs?

OpenSEO uses **Cloudflare Workers cron triggers** to run background maintenance and data processing tasks. The system defines schedules in `wrangler.jsonc` and handles execution branching in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts). Each job serves a distinct operational purpose, from security hygiene to real-time ranking data collection.

| Cron Expression | Schedule | Job Name | Primary Purpose |
|---------------|----------|----------|-----------------|
| `17 3 * * *` | Daily at 3:17 AM UTC | MCP OAuth Purge | Remove stale OAuth credentials and revoke expired tokens |
| `*/5 * * * *` | Every 5 minutes | Scheduled Rank Checks | Poll rank-tracking queue and trigger checks per organization |
| `* * * * *` | Every minute | Audit Watchdog | Reconcile stale audits that became stuck during processing |
| `*/15 * * * *` | Every 15 minutes | Rank-Check Guard | Kill overlapping poll windows to prevent runaway loops |

## MCP OAuth Purge Cron (3:17 AM Daily)

The **MCP OAuth purge** job handles security maintenance by cleaning up expired authentication state.

### Source Location and Implementation

In [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) at lines 191-200, the server checks the incoming cron trigger and dispatches to the OAuth cleanup handler:

```typescript
// src/server.ts (lines 191-200)
if (controller.cron === MCP_OAUTH_PURGE_CRON) {
  // Purge stale OAuth credentials
  // Revoke expired access tokens from storage
  // Clean up abandoned authorization flows
}

```

### Why 3:17 AM?

The **3:17 AM UTC** timing spreads load across Cloudflare's infrastructure by avoiding common midnight/2:00 AM windows. This reduces contention with other Workers users while ensuring daily cleanup completes before peak usage hours.

### Security Impact

- **Prevents token replay attacks** by proactively removing expired credentials
- **Reduces storage costs** by pruning abandoned authorization flows
- **Maintains compliance** with OAuth 2.0 token lifetime requirements

## Scheduled Rank Checks Cron (Every 5 Minutes)

The **rank-tracking polling loop** drives OpenSEO's core SEO monitoring feature, checking search positions for tracked keywords across configured organizations.

### Source Location

Implemented 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):

```typescript
// src/server/features/rank-tracking/services/scheduledRankChecks.ts
export async function scheduledRankChecks(controller: Controller, env: Env) {
  while (true) {
    // Claim work for the current configuration
    const work = await claimRankCheckWork(env);
    if (!work) break; // No work available
    
    // Enforce per-organization plan limits
    const limits = await checkPlanLimits(work.organizationId, env);
    if (limits.exceeded) {
      await deferWork(work, limits.resetTime);
      continue;
    }
    
    // Schedule next check eagerly
    await triggerRankCheck(work, env);
    await scheduleNextCheck(work, env);
  }
}

```

### Key Design Patterns

- **Work-claiming pattern**: Prevents duplicate processing across concurrent Workers instances
- **Plan limit enforcement**: Checks organization subscription tiers before executing expensive rank queries
- **Eager scheduling**: Queues the next check immediately to maintain cadence even with variable processing time

## Audit Watchdog Cron (Every Minute)

The **audit reconciliation** job acts as a safety net for the site audit pipeline, detecting and recovering from stuck or orphaned audit processes.

### Source Location

Implemented in [`src/server/features/audit/services/auditReconciler.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/auditReconciler.ts):

```typescript
// src/server/features/audit/services/auditReconciler.ts
export async function reconcileStaleAudits(controller: Controller, env: Env) {
  // Find audits stuck in 'running' state beyond timeout threshold
  const staleAudits = await findStaleAudits(env, STALE_THRESHOLD_MINUTES);
  
  for (const audit of staleAudits) {
    // Determine failure mode from worker logs
    const diagnosis = await diagnoseStall(audit, env);
    
    // Recover or fail gracefully based on diagnosis
    if (diagnosis.recoverable) {
      await resumeAudit(audit, env);
    } else {
      await markFailed(audit, diagnosis.error, env);
    }
  }
}

```

### Stall Detection Logic

The watchdog identifies stuck audits by:

1. **Timestamp comparison**: Audits running longer than `STALE_THRESHOLD_MINUTES` (typically 30 minutes)
2. **Heartbeat absence**: No progress updates written to the audit log stream
3. **Worker death detection**: Associated Cloudflare Worker invocation no longer active

## Rank-Check Guard Cron (Every 15 Minutes)

Though not a primary business task, this **sub-hourly guard** prevents a failure mode in the 5-minute polling loop. Referenced in code comments within [`scheduledRankChecks.ts`](https://github.com/every-app/open-seo/blob/main/scheduledRankChecks.ts), it ensures:

```typescript
// Defensive timeout within the 5-minute poll loop
const POLL_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
setTimeout(() => controller.abort(), POLL_WINDOW_MS);

```

### Overlap Prevention

- Kills long-running rank-check iterations that exceed the 15-minute safety bound
- Prevents cascading queue congestion if an external API degrades
- Ensures fresh Worker instances spin up for each cron invocation

## Cron Configuration in wrangler.jsonc

All schedules are declared in the Cloudflare Workers configuration:

```jsonc
// wrangler.jsonc
{
  "triggers": {
    "crons": [
      "17 3 * * *",      // MCP_OAUTH_PURGE_CRON
      "*/5 * * * *",     // SCHEDULED_RANK_CHECKS_CRON
      "* * * * *",       // AUDIT_RECONCILER_CRON
      "*/15 * * * *"     // RANK_CHECK_GUARD_CRON (implicit)
    ]
  }
}

```

The server maps these expressions to constants in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) for dispatch:

```typescript
// src/server.ts
const MCP_OAUTH_PURGE_CRON = "17 3 * * *";
const SCHEDULED_RANK_CHECKS_CRON = "*/5 * * * *";
const AUDIT_RECONCILER_CRON = "* * * * *";

```

## Debugging Cron Jobs in Development

To trace cron execution locally:

```bash

# Simulate specific cron trigger

npx wrangler dev --test-scheduled

# Trigger OAuth purge manually

curl "http://localhost:8787/__scheduled?cron=17+3+*+*+*"

```

### Monitoring Production Execution

| Metric | Source | Alert Threshold |
|--------|--------|---------------|
| Cron invocation count | Cloudflare Workers analytics | < 95% of expected runs per hour |
| Duration p99 | Workers tail logs | > 50 seconds for any 1-minute cron |
| Error rate | `controller.error` captures | > 0.1% of invocations |

## Summary

- **Four cron jobs** power OpenSEO's background operations, configured in `wrangler.jsonc` and dispatched from [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts)
- **MCP OAuth purge** (`17 3 * * *`) cleans authentication state daily at 3:17 AM UTC
- **Scheduled rank checks** (`*/5 * * * *`) poll and execute keyword position monitoring every 5 minutes with plan-limit enforcement
- **Audit watchdog** (`* * * * *`) reconciles stuck audits every minute to maintain pipeline health
- **15-minute guard** prevents runaway rank-check loops from congesting the queue

## Frequently Asked Questions

### How do I add a new cron job to OpenSEO?

Add your cron expression to `wrangler.jsonc` under `triggers.crons`, then add a corresponding constant and handler branch in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts). Test locally with `wrangler dev --test-scheduled` before deploying.

### What happens if a cron job fails?

Failed cron invocations surface in Cloudflare Workers tail logs with `controller.error` populated. The audit watchdog and rank-check services implement idempotent work-claiming, so transient failures resume safely on the next cron tick without duplicate processing.

### Can I change the OAuth purge time from 3:17 AM?

Yes—modify the expression in `wrangler.jsonc` and update the `MCP_OAUTH_PURGE_CRON` constant in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts). Cloudflare recommends avoiding exact hour boundaries; the 17-minute offset exists specifically to distribute load.

### Why does the rank checker need to run every 5 minutes rather than on-demand?

The polling design accommodates **hundreds of organizations** with varying check frequencies and plan limits. The 5-minute loop batches rate-limited external API calls efficiently while respecting per-organization concurrency constraints that pure event-driven architecture cannot guarantee.