ADR 0006: A request does no slow or privileged work; it queues it
Status
Accepted.
Context
Three ACME operations used to do their real work inside the HTTP request:
POST /chall/{id}awaited the challenge validation. That reached out to an address the client named, over DNS, HTTP or TLS, to a host that may never answer. It held an admission permit for the whole ofchallenge.timeout_ms. Up toserver.max_concurrent_requestsclients pointing at black-holed addresses could pin every permit. It also forced a startup rule that the request timeout exceed the validation timeout.finalizecalled the signing backend. The process parsing untrusted JWS and CSRs from the internet therefore heldca.key, a PKCS#11 login or a relay’s upstream account.- Revocation called the backend too, from
POST /revokeCert, from the admin panel, and fromorder revokeon the host. Run beside a live server, the last of these could silently drop a revocation from the CRL.
RFC 8555 already has the states that queued work needs. A challenge is
processing while “the server is working on it” (§7.1.6), an order is
processing while “the certificate is being issued” (§7.4), and §8.2 pairs both
with Retry-After. certbot, acme.sh and lego all poll.
Decision
A request claims, queues and answers. The worker does the work.
- Validation.
POST /chall/{id}claims the challenge (pending → processing, a compare-and-swap), writes achallenge_validatejob, and answers200with the challenge readingprocessingplusRetry-After. Seecrates/protocol/src/acme/validate.rs. - Issuance.
finalizechecks the CSR and runs the filter synchronously. It then claims the order and queuessigner_issuein one transaction, and answersprocessingfor every backend. Seecrates/protocol/src/acme/issue.rs. - Revocation. A local CA’s revocation is a
revocationsrow and the order’s stamp in one transaction; the worker then signs the CRL. A relay orcustomrevocation is asigner_revokejob that the request waits on, for up toserver.request_timeout_msless a second, answering503+Retry-Afterif the job is still running. Seecrates/protocol/src/acme/revoke.rs. - A verdict is terminal. A check that ran records its answer, pass or fail.
A job is retried only when the attempt could not happen at all: the database
is unreachable, or this process does not mount the profile. A backend’s own
BadCsrmakes the orderinvalid, notreadyagain, because the client was already toldprocessingand polls for a terminal state. - No stranded claim. A failed enqueue releases its claim.
abandonrecords a failure rather than leaving the client polling forever.recoverre-queues a challenge leftprocessingwith no live job. - Each kind has one handler, covering every profile. A row names its subject, the subject names its profile, and the profile names its validators or backend. The job registry refuses a second handler for a kind anyway.
Consequences
challenge.timeout_msbounds a job attempt, not an HTTP request. The only timeout rule left at startup concernssigner.custom’s read hooks, the one backend call still made inline.- A client sees
processingon every finalize, including against a local CA. This shipped under### Breaking. - The request path never names a signing backend, which is what lets the CA key leave the ACME process entirely (ADR 0007).
- A request no longer holds a permit during outbound I/O to a host the client chose.
- The integration harness runs a real worker. A test that triggers or finalizes must poll for the outcome rather than read it from the response.
Enforced by
the_request_path_never_holds_a_signerandthe_cli_never_builds_a_signerintests/layering.rs.check_request_timeout, the remaining startup refusal.tests/challenges.rs,tests/orders.rsandtests/revoke_cert.rs, which drive each operation through the queue.