Correction (2026-09-28): an earlier version of this post led with "one parcel in six" still out after
24 hours and "one in seven" getting a second scan. Those were the per-carrier average, not the rate a
parcel actually experiences - the pool is dominated by domestic US volume, where only ~4% run past a day.
Re-measured per parcel: median ~6h, ~1 in 20 past 24h, ~1 in 40 with a second scan. The per-carrier table
below was correct and is unchanged; it is the one to design thresholds against.
Most post-purchase stacks fire the same customer notification on the same trigger: the carrier posts an out_for_delivery status, you send "Arriving today!". It is one of the highest-open messages you will ever send, and on a meaningful slice of parcels it is a promise you cannot keep.
Here is the measurement, and a handler that does not lie.
The distribution, not the average
Across delivered parcels on 24hTrack, measuring from the first out-for-delivery scan to the delivery scan:
- median: about 6 hours
- 80th percentile: about 9 hours
- still undelivered 24 hours later: roughly 1 in 20
- picks up a second out-for-delivery scan: roughly 1 in 40
So "today" is right most of the time. The problem is that the tail is not evenly spread across carriers - it is concentrated, and it is predictable per carrier.
| lane | median OFD to delivery | still out after 24h |
|---|---|---|
| USPS | ~6 h | ~4% |
| UniUni | ~6 h | ~12% |
| Yanwen Express | ~8 h | ~20% |
| China Post | ~7 h | ~27% |
| a cross-border consolidator | ~25 h | ~65% |
That last row is the whole point. Same three words. On that lane, "out for delivery" is attached when the parcel reaches the local depot, not when it leaves on a van. Two thirds of those parcels are still not delivered a day later. If you send "arriving today" there, you are generating tomorrow's ticket yourself.
Stop treating the status as the event
The status is a label. What you actually want is three booleans derived from the event list.
const OFD = /out for delivery|on vehicle for delivery|with delivery courier/i;
function deliveryAttempt(events) {
const ofd = events.filter(e => OFD.test(e.description));
return {
// more than one OFD scan = it went out, came back, and went out again
retry: ofd.length >= 2,
// the carrier usually names the cause on the failed attempt
reason: events.find(e =>
/unable to deliver|no answer|not answering|address (could not|incorrect|insufficient)|nobody|no access/i
.test(e.description)),
firstOfdAt: ofd[0]?.at,
};
}
retry is the single most useful signal in the whole timeline and almost nobody surfaces it. A second out-for-delivery scan means the first attempt failed. The customer does not need reassurance at that point, they need an instruction - and the carrier has usually already written the reason into the event text.
Note what the reasons have in common: no answer at the buzzer, address insufficient, nobody to sign. None of those are fixable by the courier on the phone with the recipient. They are fixable by whoever paid for the label, because that is who the courier takes instructions from. So the correct action is "message the merchant", not "call the depot" - which is also the cheapest possible resolution for you.
Threshold per carrier, not per tenant
One global "escalate after N hours" rule cannot fit both a 6-hour lane and a 25-hour lane. Derive it:
// p90 of (delivery - first OFD), computed per carrier, on DELIVERED parcels only
const stallThreshold = carrierP90.get(carrier) ?? GLOBAL_P90;
const stalled = !delivered && hoursSince(firstOfdAt) > stallThreshold;
Two implementation notes that bite:
Only measure on delivered parcels. If you include in-flight ones you are sampling right-censored data and your p90 walks upward forever.
Keep the raw carrier text. Both the retry reason match and the handover match below run on the carrier's own sentence. If your pipeline normalises everything to an enum on write, you have thrown away the only field that can tell the customer why, and you cannot recompute it later.
The third case: the page froze, the parcel did not
On cross-border orders a local carrier takes over the last leg. The originating carrier no longer has the box, so its feed stops - and very often the last thing it published was out for delivery. Now you have a status that is simultaneously stale and alarming.
const handover = /handed over|transferred to|delivery partner|local (carrier|partner)|onward carrier/i;
const frozen = OFD.test(last.description)
&& hoursSince(last.at) > 48
&& events.some(e => handover.test(e.description));
If frozen, the useful message is not an apology about the delay. It is: this carrier handed your parcel to a local one, here is where that happened, and the parcel is very likely moving under a second number in the destination network.
What I would actually ship
- Replace the single
out_for_delivery-> "arriving today" trigger with a per-carrier one. On lanes whose median is over a day, say "at your local delivery depot" instead of "today". - Alert on
retry, not on elapsed time. A second OFD scan is a real event; four hours of silence is not. - Surface
reasonverbatim to the agent, and route those tickets to "contact the merchant" rather than to the carrier. - Compute p90 per carrier on delivered parcels, refresh it monthly, and treat anything past it as stalled.
If you want to eyeball the timelines before you write any of this, 24hTrack (24htrack.com) is a free package tracker for 3,200+ carriers: paste any tracking number, the carrier is detected automatically, no sign-up. It is useful here mostly because you can put a USPS number and a cross-border number side by side and watch the same three words mean two different things.
I work on 24hTrack. Numbers above are medians and percentages measured on parcels tracked there; no absolute volumes. Written with AI assistance.
Top comments (0)