# Audit Limits in OpenSEO and How Metered Tasks Are Tracked Across Batches

> Discover OpenSEO audit limits and how metered tasks are tracked across batches. Learn about tier-based limits and DataForSEO credit consumption.

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

---

**OpenSEO enforces tier-based audit limits via hard-coded constants in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts) and tracks DataForSEO credit consumption per batch using the `meter` wrapper in [`src/server/lib/dataforseo/client.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/client.ts).**

The `every-app/open-seo` repository implements a dual-layer constraint system: hard limits on crawl scope and concurrent capacity per tier, plus a granular metering layer for external API costs. This architecture ensures that free-plan users cannot abuse compute resources while accurately attributing DataForSEO credit spend to the correct organization and feature.

## Understanding OpenSEO Audit Limits

### Hard-Coded Page Bounds

OpenSEO defines four immutable constants that govern audit granularity. These values live in **[[`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts)** and are referenced by both client-side forms and server-side validation:

- **`MIN_AUDIT_PAGES`** (10): The absolute floor for any audit request.
- **`DEFAULT_AUDIT_PAGES`** (50): Fallback value when users omit a page limit.
- **`FREE_MAX_AUDIT_PAGES`** (50): Hard ceiling for free-tier audits.
- **`PAID_MAX_AUDIT_PAGES`** (10,000): Ceiling for paid and self-hosted deployments.

These constants prevent drift between the launch form’s UI constraints and the workflow’s execution parameters.

### Tier-Specific Capacity Constraints

Beyond per-audit page caps, OpenSEO enforces organizational limits via the `AUDIT_LIMITS` record in **[[`src/server/features/audit/services/audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts)**:

| Tier | `maxPagesPerAudit` | `maxCapacityUnits` | `maxRunningAudits` |
|------|-------------------|-------------------|--------------------|
| **free** | 50 | 2,000 | 5 |
| **paid** | 10,000 | 100,000 | Unlimited |
| **self_hosted** | 10,000 | Unlimited | Unlimited |

Free organizations hit a hard stop at five concurrent audits and 2,000 total capacity units, while paid plans scale to 100,000 units with unlimited concurrency. Self-hosted installations bypass capacity and concurrency checks entirely.

### Normalizing User Input with `clampAuditMaxPages`

When a client submits a custom page limit, the server normalizes it through **`clampAuditMaxPages`** (also in [`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts)). This utility clamps the input to the inclusive range `[MIN_AUDIT_PAGES, PAID_MAX_AUDIT_PAGES]` and substitutes `DEFAULT_AUDIT_PAGES` if the value is null or undefined.

```typescript
import { clampAuditMaxPages } from "@/server/features/audit/services/audit-capacity";

const userPages = 300; // Potentially from a query parameter
const pagesToCrawl = clampAuditMaxPages(userPages);
// Result: 300 (within the allowed 10–10,000 range)

```

## How Metered Tasks Are Tracked Across Batches

### The Metering Layer Architecture

OpenSEO integrates with **DataForSEO** for credit-metered data such as keyword ideas, backlinks, and Lighthouse results. The metering logic resides in **[[`src/server/lib/dataforseo/client.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/client.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/client.ts)** and follows this flow:

1. **`createDataforseoClient`** instantiates a namespaced client backed by a billing customer context.
2. The **`meter`** function wraps every endpoint call, returning a proxy that invokes **`meterDataforseoCall`** upon execution.
3. **`meterDataforseoCall`** executes the underlying fetcher, then records the cost via `trackDataforseoCost` if running in hosted mode.
4. Special error handling catches `DataforseoChargedTaskError` to log spend even when the request was malformed but still billed.

### Batch-Level Charging for DataForSEO

For endpoints accepting array inputs—such as **`postRankCheckTasks`**—OpenSEO writes a **single billing entry for the entire batch**. As documented in [`client.ts`](https://github.com/every-app/open-seo/blob/main/client.ts), “one metered charge covers the whole batch (DataForSEO bills task_post at post time, collection is free).” This means posting 100 keywords incurs one charge, not 100 individual charges.

```typescript
import { createDataforseoClient } from "@/server/lib/dataforseo/client";
import { getOrCreateOrganizationCustomer } from "@/server/billing/subscription";

async function runRankCheckBatch(customerCtx, keywordBatch) {
  const billingCustomer = await getOrCreateOrganizationCustomer(customerCtx);
  const dfClient = createDataforseoClient(billingCustomer);

  // Posts up to 100 queued tasks; one metered charge covers the whole batch
  await dfClient.serp.rankCheckTaskPost({ keywords: keywordBatch });
}

```

### Non-Metered vs. Metered Operations

The **[[`siteAuditWorkflowCrawl.ts`](https://github.com/every-app/open-seo/blob/main/siteAuditWorkflowCrawl.ts)](https://github.com/every-app/open-seo/blob/main/src/server/workflows/siteAuditWorkflowCrawl.ts)** workflow demonstrates the boundary between free and metered compute. The actual crawl runs on Cloudflare Workers and **consumes zero DataForSEO credits**. Only downstream calls—such as fetching Lighthouse metrics or keyword data—trigger the metering layer. This design ensures that partial crawl failures do not generate orphaned credit charges.

## Summary

- **Audit limits in OpenSEO** are enforced through constants in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts), with tier-specific capacity rules defined in [`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts).
- Free plans are capped at 50 pages per audit, 2,000 capacity units, and 5 concurrent runs; paid and self-hosted plans scale to 10,000 pages with higher or unlimited thresholds.
- The **`clampAuditMaxPages`** function normalizes user input to prevent out-of-bounds requests.
- **Metered tasks** are tracked via the `meter` wrapper in [`src/server/lib/dataforseo/client.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/client.ts), which records credit usage per batch rather than per individual task.
- Crawl operations use Workers compute and remain un-metered; only DataForSEO API calls incur credit charges.

## Frequently Asked Questions

### What are the OpenSEO audit limits for free versus paid plans?

Free-tier organizations are limited to 50 pages per audit, 2,000 total capacity units, and a maximum of five running audits simultaneously. Paid plans allow up to 10,000 pages per audit, 100,000 capacity units, and unlimited concurrent audits. Self-hosted installations inherit the 10,000-page limit but bypass capacity and concurrency restrictions entirely.

### How does OpenSEO handle metered task tracking for batch operations?

OpenSEO tracks metered tasks through a wrapper function called `meter` in [`src/server/lib/dataforseo/client.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/client.ts). When you post a batch of tasks—such as 100 keyword checks—the system records a **single billing entry** for the entire batch. DataForSEO bills at post time, making the collection phase free, and the `meterDataforseoCall` function handles special cases like malformed requests that still incur charges.

### What happens if a user requests more pages than the OpenSEO audit limit allows?

The `clampAuditMaxPages` function in [`src/server/features/audit/services/audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) automatically clamps the requested value to the range of 10 to 10,000 pages. If the user submits a number below the minimum, it defaults to 10; if above the maximum, it caps at 10,000. If no value is provided, the system falls back to 50 pages.

### Are website crawl operations in OpenSEO metered against DataForSEO credits?

No. The crawl phase executed in [`src/server/workflows/siteAuditWorkflowCrawl.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/siteAuditWorkflowCrawl.ts) runs on Workers compute and does **not** consume DataForSEO credits. Only subsequent calls to DataForSEO endpoints—such as retrieving Lighthouse scores or backlink data—trigger the metering layer and deduct credits from the organization’s balance.