# How the Audit Capacity Tier System Limits Crawling in OpenSEO

> Discover how the audit capacity tier system in OpenSEO restricts crawling via page caps, capacity units, and concurrent audit limits. Learn about maxPagesPerAudit, maxCapacityUnits, and maxRunningAudits.

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

---

**The audit capacity tier system limits crawling by enforcing plan-specific caps on pages per audit, total capacity units consumed, and concurrent running audits through `maxPagesPerAudit`, `maxCapacityUnits`, and `maxRunningAudits` constraints.**

OpenSEO's site audit workflow needs guardrails to prevent resource exhaustion. The platform implements a **three-tier capacity system**—free, paid, and self-hosted—that clamps crawl scope before any HTTP request leaves the server. This article explains exactly where those limits live in the codebase and how they gate the crawling pipeline.

---

## Where Capacity Limits Are Defined

### The AUDIT_LIMITS Record

All tier configurations reside 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). The system defines a `Record<AuditLimitTier, TierLimits>` mapping each tier to its three enforcement levers:

```typescript
export const AUDIT_LIMITS: Record<
  AuditLimitTier,
  { maxPagesPerAudit: number; maxCapacityUnits: number; maxRunningAudits: number }
> = {
  free: { 
    maxPagesPerAudit: FREE_MAX_AUDIT_PAGES, 
    maxCapacityUnits: 2_000, 
    maxRunningAudits: 1 
  },
  paid: { 
    maxPagesPerAudit: PAID_MAX_AUDIT_PAGES, 
    maxCapacityUnits: 100_000, 
    maxRunningAudits: Number.POSITIVE_INFINITY 
  },
  self_hosted: { 
    maxPagesPerAudit: PAID_MAX_AUDIT_PAGES, 
    maxCapacityUnits: Number.POSITIVE_INFINITY, 
    maxRunningAudits: Number.POSITIVE_INFINITY 
  },
};

```

The numeric constants `FREE_MAX_AUDIT_PAGES` (50) and `PAID_MAX_AUDIT_PAGES` (10,000) are declared 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), making them importable by both server and client code.

---

## Three Enforcement Points in the Crawl Pipeline

### 1. Page Count Clamping Before Estimation

The `clampAuditMaxPages` function prevents oversized requests from ever reaching the crawler. It enforces a hard floor (`MIN_AUDIT_PAGES`) and ceiling (`PAID_MAX_AUDIT_PAGES`), with paid/self-hosted tiers later restricted by their `maxPagesPerAudit` lookup:

```typescript
export function clampAuditMaxPages(maxPages?: number) {
  return Math.min(
    Math.max(maxPages ?? DEFAULT_AUDIT_PAGES, MIN_AUDIT_PAGES),
    PAID_MAX_AUDIT_PAGES,
  );
}

```

**Source:** [[`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) lines 42-46

For free tiers, this clamping effectively forces `maxPagesPerAudit` to 50, since `PAID_MAX_AUDIT_PAGES` (10,000) is reduced to `FREE_MAX_AUDIT_PAGES` during tier validation elsewhere in the pipeline.

### 2. Capacity Unit Budget Calculation

Before spawning crawl workers, the system calculates total **capacity units**—a composite metric combining page fetches plus Lighthouse analysis overhead:

```typescript
export function getEstimatedAuditCapacity(input: { 
  maxPages?: number; 
  lighthouseStrategy?: LighthouseStrategy 
}) {
  const pagesTotal = clampAuditMaxPages(input.maxPages);
  const lighthouseStrategy = input.lighthouseStrategy ?? "auto";
  const lighthouseChecks = lighthouseStrategy === "auto" ? 20 : 0;
  
  return { 
    pagesTotal, 
    lighthouseTotal: lighthouseChecks, 
    total: pagesTotal + lighthouseChecks 
  };
}

```

**Source:** [[`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) lines 49-62

A free-tier user requesting 200 pages receives a capacity estimate of 70 units (50 clamped pages + 20 Lighthouse checks), well under their 2,000-unit budget—but their page cap has already been violated, triggering an earlier rejection.

### 3. Runtime Enforcement in AuditService

The [`AuditService.start`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/AuditService.ts) method (line 55 and subsequent validation logic) performs the final gate check:

- Looks up `AUDIT_LIMITS[input.limitTier]`
- Compares `estimatedCapacity.total` against `maxCapacityUnits`
- Compares current running audit count against `maxRunningAudits`

If either threshold is exceeded, the promise rejects before any crawl state is initialized. This protects Worker compute and database connection pools from runaway audits.

---

## What Users See When Limits Hit

The client surfaces tier-aware error messaging using constants imported from the shared limits module:

```typescript
AUDIT_PAGE_LIMIT_EXCEEDED: 
  `Free plan audits are limited to ${FREE_MAX_AUDIT_PAGES} pages. Upgrade to run larger audits.`

```

**Source:** [[`src/client/lib/error-messages.ts`](https://github.com/every-app/open-seo/blob/main/src/client/lib/error-messages.ts)](https://github.com/every-app/open-seo/blob/main/src/client/lib/error-messages.ts) line 16

This single source of truth prevents drift between enforcement logic and user communication.

---

## Practical Capacity Checking Examples

### Estimating a Free-Tier Audit

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

// User requests 200 pages; clamping enforces the free tier ceiling
const estimate = getEstimatedAuditCapacity({ maxPages: 200 });
console.log(estimate.pagesTotal); // → 50
console.log(estimate.total);      // → 70 (50 + 20 Lighthouse)

```

### Validating Before Starting

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

function canStartAudit(
  tier: "free" | "paid" | "self_hosted", 
  requestedPages: number
): boolean {
  const limits = AUDIT_LIMITS[tier];
  const { total } = getEstimatedAuditCapacity({ maxPages: requestedPages });
  
  return total <= limits.maxCapacityUnits;
}

console.log(canStartAudit("paid", 9_500));      // true
console.log(canStartAudit("free", 100));        // true (capacity OK, but page cap will block)

```

---

## Key Implementation Files

| File | Responsibility | Line References |
|------|---------------|----------------|
| [[`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) | Tier constants (`FREE_MAX_AUDIT_PAGES`, `PAID_MAX_AUDIT_PAGES`) | Lines 1-20 |
| [[`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) | `AUDIT_LIMITS` record, `clampAuditMaxPages`, `getEstimatedAuditCapacity` | Lines 10-62 |
| [[`src/server/features/audit/services/AuditService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/AuditService.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/AuditService.ts) | Runtime tier lookup and limit enforcement | Line 55+ |
| [[`src/client/lib/error-messages.ts`](https://github.com/every-app/open-seo/blob/main/src/client/lib/error-messages.ts)](https://github.com/every-app/open-seo/blob/main/src/client/lib/error-messages.ts) | User-facing limit exceeded messages | Line 16 |
| [[`src/server/features/audit/services/audit-capacity.test.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.test.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.test.ts) | Unit tests verifying tier behavior across limits | Full file |

---

## Summary

- **Audit capacity tiers** (free, paid, self-hosted) govern crawling through three numeric levers: `maxPagesPerAudit`, `maxCapacityUnits`, and `maxRunningAudits`
- **Page clamping** occurs via `clampAuditMaxPages` before any capacity calculation, with paid/self-hosted tiers capped at 10,000 pages and free tiers at 50
- **Capacity units** combine page fetches plus Lighthouse overhead, enforced against tier budgets in `AuditService.start`
- **Self-hosted deployments** inherit paid page limits but remove capacity unit and concurrency caps entirely
- All constants live in [`audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/audit-limits.ts); all enforcement logic in [`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts); runtime validation in [`AuditService.ts`](https://github.com/every-app/open-seo/blob/main/AuditService.ts)

---

## Frequently Asked Questions

### How do I increase my audit page limit beyond 50?

Upgrade from the free tier to paid or self-hosted. According to the source code in [[`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts), paid tiers raise `maxPagesPerAudit` to 10,000 pages and `maxCapacityUnits` to 100,000 units, while self-hosted removes the capacity unit ceiling entirely.

### What happens if I request more pages than my tier allows?

The `clampAuditMaxPages` function silently reduces your request to the tier maximum during capacity estimation, but `AuditService` will reject the audit with an `AUDIT_PAGE_LIMIT_EXCEEDED` error before crawling begins. The error message references `FREE_MAX_AUDIT_PAGES` from [[`audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/audit-limits.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts).

### Why does the free tier allow only one concurrent audit?

The `maxRunningAudits: 1` constraint in `AUDIT_LIMITS.free` protects shared compute resources. Paid tiers set this to `Number.POSITIVE_INFINITY`, and self-hosted deployments inherit the same unlimited behavior, as implemented in [[`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) lines 14-18.

### How is capacity consumption estimated before crawling starts?

The `getEstimatedAuditCapacity` function in [[`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) sums clamped page counts with Lighthouse strategy overhead—20 units for "auto" mode. This estimate is validated against `maxCapacityUnits` without performing any actual HTTP requests, preventing wasted resources on doomed audits.