Operations / Correct under repetition
Generating a document in the request or in the background
Whether the user waits for the document, which is a decision about the document's size and the user's expectation rather than about performance.
Why the default answer is wrong here
Generating in the request is simpler in every respect: no queue, no state, no delivery step, and the user gets the file. It stops being viable when the document takes longer than a person will wait or longer than a gateway will hold a connection, and the usual mistake is to leave it synchronous until that happens in production rather than deciding in advance. The opposite mistake is equally common: putting a one-page receipt through a queue and a notification because the architecture diagram had a queue on it.
The decisions
Reasons rather than a description of the code. Each has a default that is defensible in general and wrong for documents specifically.
- Keep it synchronous when the document is small, the user is waiting, and the render is reliably within a couple of seconds. That is most single-document cases and it needs none of the machinery on the other pages.
- Move to asynchronous when the document is large, the run is many documents, or the user is not present. Those are different reasons and any one of them is sufficient.
- Decide against the tail rather than the median. A render that is usually fast and occasionally slow will time out in front of a user eventually, and the tail is what the user experiences.
- Where it is asynchronous, tell the user where the document will appear rather than leaving them to look for it, because the cost of asynchrony is paid in their uncertainty.
- Use the callback rather than polling where the caller can receive one. options.callback_url exists on a render and webhook_url exists on a batch, and both require a public http or https URL: a private or reserved address is rejected outright.
- Do not make it asynchronous to hide a slow render. That converts a visible latency problem into an invisible one, and it will still be slow.
In practice
A fragment, with the thing that goes wrong kept in a comment where it is the more instructive half.
// Synchronous: small, user waiting, reliably fast. Needs none of the
// queue machinery.
app.get("/receipts/:id.pdf", async (req, res) => {
const receipt = await db.receipts.find(req.params.id);
const out = await pdf.post("/v1/pdf", {
html: renderReceipt(receipt),
options: { format: "A4" },
});
res.type("application/pdf").send(out.body);
});
// Asynchronous: many documents, or nobody waiting. The callback is
// preferred over polling, and it must be a public URL: a private or
// reserved address is rejected as ssrf_blocked.
await pdf.post("/v1/pdf/batch", {
options: { format: "A4" },
requests: chunk.map(toRequest),
webhook_url: "https://api.example.com/hooks/documents",
webhook_secret: process.env.DOC_WEBHOOK_SECRET, // at least 16 characters
});
/* Decide against the tail, not the median. A render that is usually
800ms and occasionally 30s will time out in front of a user, and the
user's experience is the tail.
And do not go asynchronous to hide a slow render: it makes the
latency invisible rather than shorter. */What people do instead
Leaving it synchronous until a customer with more data than anyone anticipated hits the gateway timeout. The failure lands on the largest customer, which is the one you least want it to land on, and it presents as a broken page rather than as a slow document.
What the symptom looks like
Gateway timeouts on a document endpoint are the signal to change the model rather than to raise the timeout. Raising it moves the number and keeps the shape.
Frequently asked
Is this worth doing for a small volume of documents?
Some of it, and the cheap parts are the ones that matter. Allocating a document's identity before rendering it costs nothing and prevents duplicates forever. Logging the template version costs one field and answers most support questions. Queue design, backpressure and capacity planning are genuinely for scale and can wait until there is some.
Why is duplication treated as more serious than latency here?
Because a document usually carries an identity. A duplicated read is harmless and a duplicated invoice is a second numbered document for one event, which somebody has to reconcile by hand and which may already have been sent. That asymmetry is why the correctness half of this cluster is larger than it would be for most APIs.
How does this relate to the troubleshooting pages?
Those start from a symptom you are looking at right now and work back to a cause. These start from a decision made before the symptom exists. The two meet in the middle: a decision skipped here usually appears there some months later as a problem with no obvious explanation.
Related operational topics
The decisions that depend on each other, then the rest of the same group.
Receiving a callback when a document is ready, and verifying it
Being told when work finished instead of asking repeatedly, which removes polling and adds an endpoint that has to be correct about who is calling it.
Setting timeouts along the whole path, not just in the client
Making the timeouts at each hop consistent with each other, because a path whose timeouts disagree fails in the least useful way available.
Queue design for a payroll run or any large document batch
Structuring a job that produces thousands of documents at once, where the interesting decisions are about failure and progress rather than throughput.
Idempotency for a document that must not be generated twice
Making a repeated request produce the same document rather than a second one, which matters more here than in most APIs because the artefact is often numbered.
Retrying a render safely, and which failures are safe to retry
Deciding which failures should be retried and which should not, because a blanket retry on a document endpoint is a duplication mechanism.
Every operational topic
The full list, grouped by correctness, visibility and cost.
What this API actually does
The options and endpoints these decisions are built on, one page each.
Most of these decisions are cheaper to make before the first production run than after the first incident, and none of them need a large system to be worth making.