Skip to content

Build1 publisher3 min readPublished

A 100-request run makes p99 the second-slowest sample in this shipping-label harness

The rule for choosing a hosted PDF API or a local library for shipping labels rests on p95 and p99 measured under identical load, and the percentile helper the post prints reports whichever rank the sample count lands on.

The Engineer · Build desk

Illustration accompanying A 100-request run makes p99 the second-slowest sample in this shipping-label harness

What happened

  • A dev.to post argues for sending bursty, maintenance-heavy shipping-label rendering to a hosted PDF API, and keeping it local when the print path has a hard tail-latency or data-boundary requirement.
  • A hosted call adds DNS, connection setup, transit, remote queueing and a response download; a local process drops the transit and hands you fonts, native dependencies, memory ceilings, patching and burst capacity.
  • The post publishes a vendor-neutral harness: a thread pool driving a generic render callable, a %PDF- prefix check on every payload, and a percentile helper over the sorted elapsed times.
  • It warns that a hosted service can show a clean median while a concurrency limit stretches p99, and a local library can look fast until CPU throttling or font-cache misses push a worker past the printer's pickup window.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision With the same invariants binding both architectures, the choice turns on which failure surface a team can watch and control during a warehouse burst.
  • constraint A p99 pass line taken from this helper is unusable without the sample count that produced it, because at 100 requests only one sample sits above the number being reported.
  • exposure A hosted render that times out at the client but completes remotely can leave two PDF artifacts for one parcel. That surfaces in a delivery dispute, where the bytes have to match the input.
  • contradiction The post asks for queue wait measured on its own and four remote failure classes counted. The harness it prints starts its clock after the pool assigns a worker and aborts on the first non-PDF payload.

The `exercise` function submits every request to a `ThreadPoolExecutor` at once and starts `perf_counter` inside the function the pool runs [19]. The wait for a free worker slot falls outside the measured interval [3]. The post tells you to measure queue wait, rendering time and transfer time separately [15], and each `Sample` it returns holds one elapsed figure and a byte count [29]. For a hosted route, remote queueing is one of the five things the network hop adds [9], and it arrives inside that single number.

The percentile helper sorts the elapsed values and indexes at `min(len(values) - 1, int((len(values) - 1) * fraction))`, with no interpolation [22]. Ask it for 0.99 over 100 requests and it returns index 98, with exactly one sample above it [1]. Over 1,000 requests it returns index 989, with ten above [2]. The sample count belongs in the log beside the test date, runtime release, template hash, concurrency and output-size distribution the post already asks you to record [23].

Failure shape gets thinner treatment than latency. When a payload does not start with `%PDF-`, the timed function raises `ValueError` [20], and `exercise` collects with `job.result()` [21]. The first bad render aborts the run and takes the samples with it. The post asks you to classify connection failures, deadline expirations, rate limits, and successful responses that arrive after the client gives up [26]. Those four are counts, and the harness as printed returns samples or an exception.

The timeout is the invariant that matters most. If the application cannot tell whether a timed-out request completed remotely, a blind retry can create two artifacts for one label [12]. The answer given is an application-level job identity, validation of the returned bytes, and an atomic commit of the artifact reference [13]. "The renderer can be swapped later; the state transition should not change," the author wrote [14]. The four invariants come before the comparison for that reason: durable job identity with an idempotent retry key, immutable bytes with a recorded digest, a recorded parent and ordered children for every merge or split, and no silent move from pending to delivered on a timeout [4][5][6][7].

On the outcome the author declines to predict: "I am not sure which architecture will win for your templates; connection reuse, isolation, and barcode complexity can reverse the result" [24]. He also wrote that "I've learned from email and OTP delivery work that a clean median can hide a painful tail" [25]. The post publishes no measured latency figures. It supplies the harness, the corpus, and a pass line taken from upstream request deadlines, printer pickup cadence and retry windows [30]. The corpus is deliberately awkward: the longest street address, non-ASCII recipient names, every supported label size, a blank optional field, a large barcode, and the largest merge or split bundle [17]. Every candidate replays the same bytes and template revision [18].

The data-boundary half of the rule is decided before the test runs. A `Sample` carries seconds and a byte count [29]; the recipient's name and street address are in the payload either way [17]. In the edtech case the post describes, a shipment can hold a label, a course-pack manifest and several signed forms, merged for one parcel and split again when a warehouse creates two packages [27]. A signature that cannot be connected to the input, the template version and the final bytes will not help during a delivery dispute [28].

What to watch

  • Whether the author publishes p95 and p99 figures from the awkward corpus, with the sample count and concurrency attached.
  • A revision of the harness that counts outcomes where it now raises, so rate limits and late-arriving successes show up as classes.
  • Whether the four invariants get written as tests against the artifact store, or stay prose.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories