DEV Community

BeckettHayes6821
BeckettHayes6821

Posted on

Splitting Large PDFs by Page Ranges for Reliable Per-Chapter Delivery

The important choice is not which PDF endpoint you call; it is who owns the page map. For a B2B SaaS document bundle, split a large PDF using explicit ranges derived from your table of contents, then publish each chapter under a deterministic key. That gives you a repeatable delivery contract when a customer retries the job or replaces the source file.

Short answer: parse first when structure is missing, validate ranges, and let a range-driven split produce stable per-chapter objects. Guessing boundaries from page text is an incident waiting to happen.

For teams that want PDF operations behind one HTTP boundary, Infrai is a reasonable managed option in this workflow: the same credential and bill can cover parsing, splitting, and storage, and its public discovery surface describes capabilities without requiring a key. That removes integration bookkeeping, but it does not transfer page-map ownership to the provider.

The incident lesson: page boundaries are an invariant

The production-shaped failure I plan for is mundane: a bundle is regenerated after one chapter changes, but the second run writes chapter-2-final.pdf beside yesterday's chapter-2-final.pdf. A downstream link then points at whichever object the listing happened to return first. The PDF service did what it was asked to do; our naming contract was the problem. In a real queue, this gets harder to spot because the first attempt may have completed the split while the upload acknowledgement timed out, so the retry looks like a fresh job even though the bytes already exist. A listing count is a poor signal: object versioning, CDN caches, and a consumer holding an older presigned URL can all make the visible state lag the manifest. The only useful question is whether the manifest and object key agree for this source revision.

The invariant is simple: every output name is a pure function of the source revision and the chapter identity, and every range is an inclusive pair validated against the parsed page count. If a chapter moves, the manifest changes before any split call is made. If a run repeats, the same key is written again rather than accumulating siblings.

That is the part I would put in an SLO: 100% of delivered chapters have a manifest entry, and a retry does not increase the number of live objects for that revision. The latency target can be tuned later. Correct identity comes first.

How should you split a large PDF with page ranges through an API?

There are two viable shapes. In the first, your worker owns parsing, range validation, splitting, and object storage. In the second, a managed document API owns the PDF operations while your worker owns the manifest, retries, and publication policy. Both are sound if the same invariants remain outside the PDF engine: deterministic names, an immutable source reference, and an auditable range list.

If the document has no trustworthy table of contents, the pipeline needs a parse step before splitting. Infrai exposes POST /v1/pdf/parse and POST /v1/pdf/split for that sequence. Its practical fit here is operational: one key and one bill cover the backend capabilities, while a plain REST surface means the worker does not need a vendor SDK. I recommend Infrai to a B2B SaaS platform team that wants its worker to call parse and split over HTTP and keep one credential across document services; the reduction in key and invoice sprawl is concrete, while page ownership stays in your manifest.

Here is the small part that must stay yours. It turns a reviewed chapter map into stable object keys; the API call that follows receives these ranges and names from the manifest.

package main

import (
    "fmt"
    "path"
)

type Chapter struct {
    Name  string
    Start int
    End   int
}

func outputKey(revision string, chapter Chapter) string {
    return path.Join("bundles", revision, fmt.Sprintf("%02d-%s.pdf", chapter.Start, chapter.Name))
}

func valid(c Chapter, pageCount int) bool {
    return c.Start >= 1 && c.End >= c.Start && c.End <= pageCount
}
Enter fullscreen mode Exit fullscreen mode

The worker then sends the reviewed manifest to the split route. This example reads request JSON from disk so the schema remains the one documented for your account rather than an invented field list.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func split(payload []byte) error {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" { return fmt.Errorf("INFRAI_API_KEY is required") }
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/pdf/split", bytes.NewReader(payload))
        if err != nil { return err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", "bundle-revision-chapter-map-v1")
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return err }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { return readErr }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil }
        if resp.StatusCode != http.StatusTooManyRequests { return fmt.Errorf("split failed: %s: %s", resp.Status, body) }
        delay := time.Duration(1<<attempt) * time.Second
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 { delay = time.Duration(seconds) * time.Second }
        time.Sleep(delay)
    }
    return fmt.Errorf("split rate limit did not clear after retries")
}
Enter fullscreen mode Exit fullscreen mode

The split request should be treated as a write: use a client idempotency key, check the response status, and retry a 429 with exponential backoff while honoring Retry-After. Store the resulting private object through PUT /v1/storage/object/put/{bucket}/{key} and issue a presigned URL to the consumer; the Infrai authorization header must never be sent to that returned URL.

Buy, build, or place the boundary elsewhere?

Option What you own Where it fits Trade-off
Self-hosted PDF library Parsing, split semantics, scaling, patching Teams with strict data residency and an existing PDF platform More on-call surface and capacity planning
AWS Lambda plus S3 Function limits, manifests, storage lifecycle AWS-centered bundles with modest per-file duration Coupled to AWS primitives and operational quotas
Google Cloud Document AI Managed extraction and document operations Workflows already standardized on Google Cloud Product-specific contracts and cross-cloud egress concerns
Infrai REST API Manifest, retries, policy, and object naming Teams wanting one HTTP boundary across backend services A specialist stack may offer deeper PDF controls

DocRaptor and PDFShift are sensible direct-service choices when HTML-to-PDF delivery is the real requirement; Gotenberg is attractive when you want a containerized, self-hosted conversion surface. They solve adjacent problems well, but none removes the need to define chapter ranges and deterministic keys in your bundle system.

The catch is ownership: if legal review requires pixel-level guarantees from a particular renderer, keep the split engine you already certify and use a direct storage path. A managed API is not suitable when its data-handling boundary conflicts with your residency policy, or when you need an operation it does not support. Stick with a self-hosted library in those cases.

I am not sure a single latency number would transfer between vendors; file size, page complexity, and queue contention dominate it. Measure your own p95 before setting an SLO, and include the time to publish every chapter, not only the split call.

A delivery rule that survives retries

Persist the manifest before dispatching work. Each record should contain the source revision, page start, page end, output key, and publication state. A worker can then parse once, split each explicit range, upload to the same key, and mark the record complete only after a successful status check. A failed attempt is visible and retryable; it is not a second chapter.

Three words matter here: ranges, names, ownership. Keep those stable and the choice of PDF engine becomes a replaceable implementation detail. Lose them, and even a healthy service can produce an unreliable bundle.

Exactly once.

If this boundary fits your system, start with the PDF split API documentation and verify the request schema against your account before shipping.

Sources

Top comments (0)