Admission and enqueue are separate commits
A crash between them creates a request no worker can see, or an event pointing to state that never committed. The transactional outbox closes that gap.
A browser job is slow, memory-heavy and externally visible. The queue normally provides at-least-once delivery, so a process crash can replay a capture after it rendered, uploaded or billed.
Two workers render the same request, the loser overwrites success with failure, a webhook fires twice, or usage contains a charge with no successful request left to justify it.
Use the database as the ownership boundary: request plus outbox in one transaction, atomic state transition for the lease, and unique constraints for each externally visible effect.
const claimed = await db.$executeRaw`
UPDATE requests
SET status = 'running', started_at = COALESCE(started_at, now())
WHERE id = ${requestId}
AND status IN ('accepted', 'queued')
`;
if (claimed === 0) return currentTerminalResult(requestId);
try {
const artifact = await render(requestId);
await db.$transaction(async (tx) => {
await tx.request.update({ where: { id: requestId }, data: { status: "succeeded" } });
await tx.usageLedger.create({ data: { request_id: requestId, units: 1 } });
});
} catch (error) {
if (isRetryable(error)) await requeueWithBackoff(requestId);
else await markFailedWithoutCharge(requestId);
}The call above is small. Everything around it is the system — and the system is what you would be signing up to own.
A crash between them creates a request no worker can see, or an event pointing to state that never committed. The transactional outbox closes that gap.
Two workers can both read queued before either writes running. Ownership must be one conditional update whose affected-row count decides the winner.
A bad selector will not improve on attempt three, while a transient network failure might. Retrying terminal work consumes capacity and delays healthy jobs.
Read-then-create races. A unique constraint and conflict handling, not an earlier SELECT, makes concurrent duplicates harmless.
One artifact and one billable ledger entry for a successful non-cached request, even if queue delivery or a caller retry occurs more than once.
One successful, non-cached capture costs one credit. Failures, including platform failures, cost zero; cache hits cost zero.
Admission exceeds the plan's start bucket; honor Retry-After.
HTTP 400 · retryable
The platform cannot safely admit work at that moment.
HTTP 503 · retryable
Swap the example for a page you care about. The runner keeps every option from this guide, and carries the whole configuration into the playground — no retyping, no starting over.
Written by PageCapture Engineering. Reviewed by PageCapture data and worker maintainers. Verified 2026-08-12 by running the published code against a fixture.
Contract tests inspect atomic bulk admission and idempotency ordering; billing tests assert no error is chargeable; the executor lease and unique-ledger design are linked to repository implementation.
Sources support the browser and API behaviors named above. PageCapture-specific limits and billing are taken from the public contract; external sources are used for the underlying browser behavior.
Defines the atomic uniqueness primitive used for idempotent effects.
Documents retry timing and stable webhook event ids.
Documents bulk Idempotency-Key replay semantics.