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.tswith constants for minimum, default, and maximum page counts - Capacity calculation in
audit-capacity.tsclamps values and estimates total workload including Lighthouse checks - Runtime enforcement in
AuditService.tsresolves 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →