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

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. 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 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:

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:

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

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:

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

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

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 with constants for minimum, default, and maximum page counts
  • Capacity calculation in audit-capacity.ts clamps values and estimates total workload including Lighthouse checks
  • Runtime enforcement in 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.

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →