Short answer: Prefer a hosted PDF API when format coverage, renderer maintenance, and burst absorption matter more than a network hop; prefer local PDF libraries when low and predictable latency, data locality, or exact control over rendering is the binding constraint.
For an e-commerce platform watermarking documents before external sharing, that decision belongs in a capacity plan, not a feature checklist. The important measurement is the full distribution of queue delay plus render time under the expected document mix. A quick median from clean one-page files won't expose what happens when large catalogs, unusual fonts, and a promotion-driven burst arrive together.
Keep the call site portable either way. The application should submit bytes, watermark policy, and a deadline to a narrow internal interface; the implementation can run a local library or call a hosted endpoint. That boundary makes verification and rollback ordinary deployment work instead of a second migration.
When should a hosted PDF API replace local PDF libraries for document format migration?
A hosted API is the stronger operational choice when the team doesn't want to own renderer patching, font packaging, conversion workers, and the queue capacity needed for bursts. It can also be a sensible bridge during document format migration, when old and new inputs coexist and the long tail of formats creates more operational work than the core watermark operation. The catch is that every document crosses another failure and latency boundary, so the service-level objective must include upload, queueing, render, download, and retry behavior rather than treating “API latency” as one indivisible number.
A local library is the stronger choice when documents must remain inside a controlled execution boundary, the supported input set is deliberately narrow, or the latency budget can't tolerate a remote round trip. Local doesn't mean free of operational cost. Native dependencies, fonts, memory peaks, temporary storage, and security updates still need owners, and render concurrency competes with the rest of the workload unless it is isolated. A library embedded in the request-serving process may look fast at low traffic and then stretch tail latency across unrelated requests under load.
Use the decision table as a review aid, not a scorecard:
| Constraint | Hosted API tends to fit | Local library tends to fit |
|---|---|---|
| Format migration | Broad or changing input set; maintenance is being bought | Small, controlled input set; behavior is pinned |
| Latency under load | Queueing can be asynchronous; bursts exceed steady capacity | Tight synchronous budget; capacity can be reserved locally |
| Data boundary | External processing is permitted by policy | Documents cannot leave the controlled environment |
| Fidelity | Provider output passes a representative golden corpus | Exact renderer, fonts, and versions must be controlled |
| On-call ownership | Team wants to own integration and SLOs, not rendering internals | Team accepts renderer, worker, and dependency ownership |
| Lock-in | Internal adapter and retained source documents permit switching | Library output and build chain are reproducible |
Neither choice wins by default.
Model latency under load before choosing
Start with arrival rate, document-size distribution, page-count distribution, and the concurrency limit of the render stage. Average render time isn't enough. A watermark pipeline that receives work faster than it completes work accumulates a queue, and queue age eventually dominates user-visible latency even when individual renders haven't slowed down. Capacity planning therefore needs a burst assumption and an admission policy: cap accepted work, shed optional work, or move external sharing to an asynchronous state while the watermark is produced.
Track at least request age, queue age, render duration, total duration, input bytes, output bytes, page count when it is available, attempt count, and execution mode. Use histograms for durations rather than averages, and split them by bounded document classes so a surge of large files doesn't hide behind many tiny ones. A hosted response such as HTTP 429 is a capacity signal; retrying it immediately from every worker amplifies contention. Respect an explicit retry delay when one is supplied, otherwise use capped backoff with jitter and a deadline that stops work after the result is no longer useful. Local workers need the same discipline — an unbounded in-memory queue is just an outage with deferred symptoms.
The SLO should describe what the customer experiences. For example, define a successful watermark as a readable result produced before the sharing deadline, then measure the proportion meeting that objective over the chosen window. The exact target can't be inferred from the rendering mechanism; it has to come from the product's sharing workflow and error budget. I'm not sure a synchronous objective is defensible for every e-commerce document, because invoices and multi-page catalogs can have very different urgency, and only production traffic classification can resolve that.
Watch the tails.
Fidelity is a separate service-level indicator. Build a corpus from permitted production-shaped documents, including embedded fonts, transparency, rotated pages, annotations, and the largest accepted input. Compare page count and dimensions, extract text where that is meaningful, and render pages to images for visual comparison with a documented tolerance. A byte-for-byte comparison is usually the wrong oracle for independently produced PDFs because metadata and object layout can differ while the visible document remains acceptable. The release gate should instead express the failures the business actually cares about: missing watermark, unreadable text, clipped content, shifted layout, or a file that downstream readers reject.
Put a safe execution boundary around rendering
The application needs one contract and two replaceable executors. Keep policy outside the renderer: validate the allowed media type and size before admission, derive an idempotency key from stable job identity, set a deadline, and write the completed object only after validation passes. Browser or worker code may hold a document in a Blob; MDN defines a Blob as immutable raw data and documents conversion to an ArrayBuffer, which is a useful transport boundary but says nothing about PDF correctness.
The Go sketch below deliberately accepts the hosted endpoint as configuration rather than baking a vendor route into application code. Both implementations return bytes through the same interface, and the caller owns the timeout. The local function is injected because library APIs and supported PDF operations differ; pretending that they share a universal watermark call would hide the very compatibility work this design is meant to isolate.
package watermark
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
)
type Request struct {
PDF []byte
Label string
JobID string
MaxOutput int64
}
type Renderer interface {
Watermark(context.Context, Request) ([]byte, error)
}
type Hosted struct {
Endpoint string
Client *http.Client
}
func (h Hosted) Watermark(ctx context.Context, in Request) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.Endpoint, bytes.NewReader(in.PDF))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/pdf")
req.Header.Set("Idempotency-Key", in.JobID)
req.Header.Set("X-Watermark-Label", in.Label)
resp, err := h.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("watermark request returned status %d", resp.StatusCode)
}
out, err := io.ReadAll(io.LimitReader(resp.Body, in.MaxOutput+1))
if err != nil {
return nil, err
}
if int64(len(out)) > in.MaxOutput {
return nil, fmt.Errorf("watermarked document exceeds output limit")
}
return out, nil
}
type Local struct {
Apply func(context.Context, []byte, string) ([]byte, error)
}
func (l Local) Watermark(ctx context.Context, in Request) ([]byte, error) {
return l.Apply(ctx, in.PDF, in.Label)
}
Don't treat this adapter as complete production validation. Authenticate a hosted request using the selected service's documented mechanism, keep credentials out of document metadata and logs, verify that the returned media really is an acceptable PDF, and bind input and output limits to the capacity model. For local execution, isolate renderers in workers with explicit CPU, memory, and temporary-storage limits. For either path, store source and result under different immutable object names, then update the sharing pointer atomically after checks pass.
One subtle failure mode deserves a longer look. Suppose a customer requests an external share, a worker times out after sending the document, and the job is retried. Without stable job identity, the second attempt can create another output while the first is still finishing; if completion order controls the public pointer, an older policy or watermark label can overwrite the newer result. The safe sequence is to assign identity before enqueueing, make attempts converge on the same logical result, validate output before publication, and reject a stale completion when the document's policy version has advanced. This isn't specific to hosted execution, but a network boundary makes ambiguous completion more visible. The same race exists when a local worker is terminated after writing bytes but before acknowledging its queue message.
Verify the migration without spending the error budget
Begin in shadow mode on documents that policy permits the candidate path to process. Produce the candidate result without publishing it, compare it against the established path using the fidelity checks, and record latency by document class. Shadow traffic consumes real capacity, so cap it and keep it outside the customer-facing deadline. Once the corpus is clean, canary by a stable key such as account or document identifier; random selection per retry risks sending one logical job through both paths.
Before increasing the canary, check four gates: fidelity failures remain within the agreed threshold, end-to-end latency meets the SLO for each important class, retry volume is bounded, and worker saturation leaves documented headroom. Those thresholds are workload policy, not universal PDF numbers. Your mileage may vary, especially when documents contain font subsets or high-resolution images that make file size a poor proxy for render work.
Verification must include the consumer side. Open the result with the readers and downstream systems the business supports, confirm that the watermark is visible without obscuring required content, and verify that access control still points only to the validated object. Keep structured logs free of document bodies and watermark text if that text can contain customer data; correlate with job identity instead.
Small canaries first.
Roll back the executor, not the document history
Rollback should be a routing change at the internal renderer boundary. Stop new admissions to the candidate, let bounded in-flight work finish or expire, and route subsequent jobs to the established executor. Preserve the source document, policy version, logical job identity, and validation outcome so the established path can reproduce the intended result without guessing. Never repoint an external share to an unvalidated intermediate object.
A hosted API is not suitable when policy forbids external document processing or when its measured tail latency cannot fit the sharing deadline; stick with isolated local workers in those cases. A local library is not suitable when the team cannot own its native dependency lifecycle, format edge cases, and burst capacity; use a hosted boundary when those duties outweigh the added network and supplier dependency. For mixed workloads, routing a narrow, stable format class locally and sending the migration long tail through a hosted executor can be reasonable, but it doubles verification paths and on-call surface. Make that complexity earn its place in the SLO data.
The final decision is operational: choose the executor whose measured fidelity and tail latency satisfy the workload, whose failure modes fit the rollback plan, and whose ownership cost the team is actually willing to carry.