01 · Diagnose the problem

Queue, retry and idempotency for browser jobs

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.

4 min readPublished Updated
What you actually see

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.

Reproduce it

  1. 01Kill the worker after artifact upload but before queue acknowledgement and observe which effects repeat.
  2. 02Race a synchronous handler and a queue worker against the same request id.
  3. 03Send the same bulk admission twice concurrently with one idempotency key; a read-before-write check should fail this test.
02 / Do it yourself

Let the database decide who owns the job

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.

claim-job.mts
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);
}
03 / Where it breaks

Where at-least-once delivery duplicates work

The call above is small. Everything around it is the system — and the system is what you would be signing up to own.

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.

The lease is only an application check

Two workers can both read queued before either writes running. Ownership must be one conditional update whose affected-row count decides the winner.

Retry policy ignores error class

A bad selector will not improve on attempt three, while a transient network failure might. Retrying terminal work consumes capacity and delays healthy jobs.

Idempotency is checked before insert

Read-then-create races. A unique constraint and conflict handling, not an earlier SELECT, makes concurrent duplicates harmless.

Keep the DIY version when

  • Jobs are manual, single-process and no effect occurs outside the local filesystem.
  • A duplicate artifact and duplicate notification are acceptable and billing is not attached to completion.
04 / The recipe

Queue retry idempotency for browser jobs

The public capture remains one request. Internally, acceptance and enqueue commit together; only the executor that wins the atomic lease renders, and credit commits in the transaction that marks success.

Capture settings
Enter the source, choose your options, then run the capture.
Result
Your capture will appear here and stay in view.
waiting

No result yet

Complete the settings and run the tool. Images, PDFs, text, and video all preview in this panel.

Send this exact request
cURL, TypeScript and Python are generated from the same configuration as the demo.
curl --fail-with-body "https://api.pagecapture.dev/v1/take" \
  -H "X-Access-Key: $PAGECAPTURE_KEY" \
  -H "Content-Type: application/json" \
  --data '{"url":"https://example.com","format":"png","response_type":"by_format"}' \
  -o capture.png
05 / Output, limits, cost, failures

One artifact and one charge, however many retries

Output

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.

Cost

One successful, non-cached capture costs one credit. Failures, including platform failures, cost zero; cache hits cost zero.

1 creditper successful non-cached capture

Limits

  • Idempotency prevents duplicate effects; it does not guarantee exactly-once execution at the transport layer.
  • Retryable errors are bounded by a maximum attempt count and scheduled with backoff.
  • Webhook delivery has its own stable event id because capture success and callback success are separate effects.

Common errors

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

06 / Take it for a run

Try it against your own page

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.

Return to the configured runner
07 / Verification
Tested and reviewed

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.

engine-crunknown-pwunknown-f2026-07-1-b2026-08-1StatusAsync delivery referenceEditorial method

Sources and verification basis

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.

  1. Defines the atomic uniqueness primitive used for idempotent effects.

  2. Documents retry timing and stable webhook event ids.

  3. Bulk capturesPageCapture

    Documents bulk Idempotency-Key replay semantics.