# How to Understand Audit Limits in OpenSEO: Enforcement, Tiers, and Configuration

> Understand OpenSEO audit limits. Learn how tiered capacity controls enforce page counts, estimate workload, and validate usage at runtime to prevent resource abuse.

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

---

**OpenSEO enforces audit limits through tiered capacity controls that clamp page counts, estimate total workload, and validate usage at runtime to prevent resource abuse.**

The **audit limits** in the every-app/open-seo repository define how many pages a site audit can crawl, how many checks it can run, and how much concurrent workload a user can queue. These limits differ between free and paid plans and are enforced through a three-layer system: static constants, capacity calculation, and runtime validation.

## Where Audit Limits Are Defined

All limit constants live in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts). This file exports values that serve as the single source of truth across the codebase.

| Constant | Value | Purpose |
|----------|-------|---------|
| `MIN_AUDIT_PAGES` | 10 | Prevents trivially small audits |
| `DEFAULT_AUDIT_PAGES` | 50 | Fallback when no page count specified |
| `FREE_MAX_AUDIT_PAGES` | 50 | Hard ceiling for free-tier audits |
| `PAID_MAX_AUDIT_PAGES` | 10,000 | Maximum pages for paid subscribers |

These constants are imported by both client-side validators and server-side enforcement logic, ensuring consistent behavior across the application.

## How Capacity Gets Calculated

The [`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) module translates raw page requests into validated audit capacity. It exports two key functions used throughout the audit lifecycle.

### Clamping Page Counts

The `clampAuditMaxPages` function ensures any user-provided value stays within acceptable bounds:

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

```

This guarantees:
- **Minimum**: At least 10 pages are always audited
- **Default**: 50 pages when unspecified
- **Maximum**: Never exceeds 10,000 (the paid tier ceiling)

### Estimating Total Workload

The `getEstimatedAuditCapacity` function computes the full resource cost of an audit, including Lighthouse performance checks:

```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,
  };
}

```

Each **"auto" Lighthouse strategy** adds 20 fixed capacity units on top of the page count. This total feeds into the tiered usage limits enforced at runtime.

## Runtime Enforcement in AuditService

The [`src/server/features/audit/services/AuditService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/AuditService.ts) orchestrates limit validation when audits are created. Enforcement happens in three sequential checks.

### Step 1: Resolve the User's Tier

`resolveAuditLimitTier` determines whether to apply free or paid limits:
- **Hosted mode**: Checks the customer's subscription status
- **Self-hosted mode**: Defaults to `"paid"` tier

### Step 2: Validate Page Limits

If the clamped page count exceeds the tier's `maxPagesPerAudit`, the service throws immediately:

```typescript
const limits = AUDIT_LIMITS[input.limitTier];
const maxPages = clampAuditMaxPages(input.maxPages);
if (maxPages > limits.maxPagesPerAudit) {
  throw new AppError("AUDIT_PAGE_LIMIT_EXCEEDED");
}

```

### Step 3: Reserve Capacity and Check Usage

After inserting the audit row, the service validates against running audit and capacity unit limits:

```typescript
const reservation = getEstimatedAuditCapacity({ maxPages, lighthouseStrategy });
// … create audit row …

const usage = await AuditRepository.getAuditUsageForUser(input.actorUserId);
if (usage.runningCount > limits.maxRunningAudits) {
  throw new AppError("AUDIT_ALREADY_RUNNING");
}
if (usage.capacityUnits > limits.maxCapacityUnits) {
  throw new AppError("AUDIT_CAPACITY_REACHED");
}

```

**Critical design choice**: Enforcement happens **after** row insertion to prevent race conditions where parallel requests could bypass limits.

## Tier Comparison: Free vs. Paid Audit Limits

| Limit | Free Tier | Paid Tier |
|-------|-----------|-----------|
| Max pages per audit | 50 | 10,000 |
| Max concurrent audits | 1 | Unlimited |
| Max capacity units | 2,000 | 100,000 |
| Lighthouse checks | Included in capacity | Included in capacity |

## Practical Code Examples

### Starting an Audit with Tier Resolution

```typescript
import { AuditService } from "@/server/features/audit/services/AuditService";

async function startUserAudit(req) {
  const { userId, organizationId, projectId, startUrl, maxPages } = req.body;

  const tier = await AuditService.resolveAuditLimitTier(organizationId);
  const result = await AuditService.startAudit({
    actorUserId: userId,
    billingCustomer: { userId, organizationId, ... },
    projectId,
    startUrl,
    maxPages,               // e.g., 200
    lighthouseStrategy: "auto",
    limitTier: tier,
  });
  return result; // { auditId: "..."} or throws AppError if limits exceeded
}

```

### Pre-Flight Limit Check

```typescript
import { AUDIT_LIMITS } from "@/shared/audit-limits";

function canRunAudit(tier: "free" | "paid", pagesRequested: number) {
  const limits = AUDIT_LIMITS[tier];
  return pagesRequested <= limits.maxPagesPerAudit;
}

// Returns true for paid plan when pagesRequested ≤ 10,000

```

## Summary

- **Audit limits** in OpenSEO are defined centrally in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts) with constants for minimum, default, and maximum page counts
- **Capacity calculation** in [`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts) clamps values and estimates total workload including Lighthouse checks
- **Runtime enforcement** in [`AuditService.ts`](https://github.com/every-app/open-seo/blob/main/AuditService.ts) resolves tiers, validates ceilings, and checks usage against concurrent and capacity limits
- **Free accounts** are capped at 50 pages, 1 running audit, and 2,000 capacity units
- **Paid accounts** support up to 10,000 pages, unlimited concurrency, and 100,000 capacity units

## Frequently Asked Questions

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

OpenSEO throws `AUDIT_PAGE_LIMIT_EXCEEDED` before creating the audit. The request is rejected immediately with no database row created.

### How does OpenSEO prevent race conditions when checking limits?

The service inserts the audit row first, then validates usage. This ensures parallel requests see each other's reservations rather than checking stale state.

### Can self-hosted deployments customize audit limits?

Self-hosted instances default to paid tier limits. The `resolveAuditLimitTier` function returns `"paid"` for non-hosted deployments, though operators could modify `AUDIT_LIMITS` constants directly in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts).

### What counts toward capacity units?

Capacity units include **clamped page count** plus **Lighthouse checks** (20 when using `"auto"` strategy). The total must stay under your tier's `maxCapacityUnits` ceiling.