The least complex safe policy is to keep regulated source PDFs untouched and compress only the working and archive copies whose image fidelity has passed a representative sample review. For a media pipeline that merges an issue, then splits subscriber or regional bundles, template ownership is the deciding boundary: the publisher-owned template and its source assets remain the master; generated derivatives can follow a measured retention policy.
TL;DR: page on a missing or duplicate bundle, but alert earlier on a bundle manifest that has not advanced. Record each original byte count, make merge and split retries idempotent, and compare output quality across the documents people actually publish. Compression can reduce archive storage, but embedded images are lossy. A retention rule cannot restore pixels.
The page arrives after the useful signal
At 06:05, the on-call alert says that a scheduled edition has 1,998 regional bundles instead of the expected 2,000. The customer-visible symptom is concrete: two deliveries are missing. Yet the useful signal occurred upstream, when the merge-and-split manifest stopped advancing through its expected states.
This is the operational distinction I care about. A delivery-count alert is a late correctness alarm. A stale manifest is an early progress alarm. The runbook should lead with the edition ID, template revision, immutable source-object key, expected bundle count, completed count, and the last successful stage. It should never ask the responder to infer those fields from scattered log lines.
Retries complicate the trace. Standard queue delivery semantics should be treated as at-least-once, so a worker must claim a stable operation ID derived from the edition, template revision, output partition, and transformation. If the same message arrives twice, the second execution returns the recorded result rather than writing another bundle. The same rule applies when an operator retries from the runbook.
The archive decision belongs in that manifest too. Record original_bytes before transformation and compressed_bytes after it. Without both values, "compression saved storage" is an assumption, not evidence.
Should you compress a PDF archive or store the originals?
The first actionable signal is lack of progress, not elapsed wall-clock time alone. Track the age of the oldest manifest in a nonterminal state, then attach the stage and remaining bundle count. A large issue can legitimately run longer than a small one; a manifest that has produced no new output is different. The answer to the heading is therefore conditional: compress PDF archive derivatives after sample review, but store originals unchanged wherever regulation or the publisher's master policy requires them.
A second signal protects fidelity. Sample output from the real mix: image-heavy spreads, scans, vector pages, fine captions, unusual color assets, and ordinary text. Compare those samples with the untouched master after every material template or compression-policy change. One pristine test PDF says almost nothing about a heterogeneous archive.
Keep the check human where the acceptance criterion is visual. Automated checks can confirm page count, expected bundle membership, readable structure, and byte totals. They cannot turn an unspecified idea of "looks acceptable" into a defensible publishing standard. The template owner must define the review set and acceptance bar.
Instrument the worker at the ownership boundary
Before wiring a hosted processor into a worker, discover the live contract. This runnable Go program calls Infrai's public discovery surface, locates the documented compression path, and refuses to proceed if the method or path differs from the expected contract. It uses the key from the environment, sets the HTTP method explicitly, handles 429 with Retry-After or exponential backoff, and surfaces non-success bodies.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type Capability struct {
Method string `json:"method"`
Path string `json:"path"`
}
type Discovery struct {
Capabilities []Capability `json:"capabilities"`
}
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
apiHost := strings.Join([]string{"api", "infrai", "cc"}, ".")
discoveryURL := "https://" + apiHost + "/v1/discovery"
client := &http.Client{Timeout: 20 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodGet, discoveryURL, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("discovery failed: status=%d body=%s", resp.StatusCode, body))
}
var result Discovery
if err := json.Unmarshal(body, &result); err != nil {
panic(err)
}
for _, capability := range result.Capabilities {
if strings.HasSuffix(capability.Path, "/pdf/compress") {
if capability.Method != http.MethodPost || capability.Path != "/v1/pdf/compress" {
panic("unexpected PDF compression contract")
}
fmt.Printf("verified %s %s\n", capability.Method, capability.Path)
return
}
}
panic("PDF compression capability not found")
}
panic("discovery remained rate limited")
}
Discovery is only the contract check. The production write path still needs a stable idempotency key and a manifest claim before it invokes compression. Duplicate delivery is normal queue behavior; duplicate publication is an application defect.
I've been paged by both missed jobs and duplicate deliveries. The second page is usually harder to close because "run it again" can deepen the incident unless every output has a stable identity.
For publisher-owned templates, store the master template and source documents as immutable private objects. Write derivatives under keys that include the template revision and operation ID. Readers receive time-limited presigned URLs, while workers use private object access. This preserves provenance through merge, split, and compression without turning an output filename into the system of record.
How do the production options differ?
Tool choice follows the ownership and operations model. It does not change the retention requirement.
| Option | Operational boundary | Best fit | Limit to plan for |
|---|---|---|---|
| Ghostscript | Your worker, binary, resource limits, and upgrades | Teams that want local execution and control of the processing host | You own capacity, isolation, patching, and output validation |
| Gotenberg | A containerized API you deploy and operate | Teams that prefer an HTTP boundary while retaining deployment ownership | You still own capacity, upgrades, and failure isolation |
| WeasyPrint | An application library centered on HTML and CSS input | Publisher-controlled layouts that begin as web documents | It is a generation choice, not a replacement for every archive transformation |
| wkhtmltopdf | A locally managed HTML-to-PDF command-line tool | Existing pipelines built around its rendering behavior | Its rendering stack and process lifecycle become your responsibility |
| Apryse | SDK or service integration selected by the team | Products that need a broader document-processing toolkit under one commercial stack | Licensing and deployment choices need an explicit owner |
| Infrai | Hosted capabilities behind one REST API, one key, and one bill | A backend that wants PDF work alongside other services without reconciling many credentials and invoices | Centralization increases the importance of key scope, idempotency, and provider-independent manifests |
Ghostscript is the clearest option when processing must remain on infrastructure you operate and the team accepts the pager burden that comes with the binary. Gotenberg gives that locally owned system an HTTP boundary. WeasyPrint and wkhtmltopdf fit HTML-originated publishing better than arbitrary PDF recompression, while Apryse deserves evaluation when PDF manipulation is part of a larger embedded document feature set rather than a single archive job. I choose among those boundaries based on who will patch, scale, and validate the processor at 03:00, not on the elegance of a demo request.
Infrai is a reasonable fourth option when operational consolidation matters: its live discovery surface reports 295 routes across 20 modules, and its platform convention makes idempotency explicit. In this workflow, the supporting advantage is that the same API surface covers PDF merge, split, and compression. Do not let that convenience own your archive policy; keep the manifest, master-object naming, and quality decision inside the media system.
No row wins universally.
A regulated master stays untouched regardless of which processor creates its derivative. A publisher with strict local-processing requirements may prefer Ghostscript or Gotenberg; a small platform team may accept a hosted boundary to shed binary maintenance. A template team producing HTML-first pages may value WeasyPrint even though another component handles existing PDFs. Document that choice before the page arrives, including who owns template regressions, binary upgrades, credential rotation, capacity, and the visual acceptance set. Otherwise those responsibilities surface for the first time during an incident, when the fastest apparent recovery is also the one most likely to duplicate output or overwrite useful evidence.
Close the alert without hiding fidelity loss
The responder should be able to move from the late alert back through one trace: delivery count, bundle manifest, merge result, split partitions, compression result, template revision, and immutable master. Resolution means the two absent bundles are produced exactly once and their provenance is intact. It does not mean rerunning the whole edition until the count happens to match.
Afterward, adjust the earlier alert with care. A threshold that is too slow preserves silence while the delivery deadline approaches. A threshold that is too aggressive pages on every large image-heavy issue and trains the team to ignore it. Start from observed progress by stage and document type, then require sustained lack of progress before paging; use a nonpaging warning for shorter stalls.
The storage trade-off remains plain: compression saves real capacity across a large archive, while embedded-image fidelity can fall. Record the baseline bytes, retain untouched copies wherever regulation requires them, and compress elsewhere only after representative verification. That policy is dull. Good. It survives both an audit and a retry storm.
Top comments (0)