Implementing Exponential Backoff for Patent APIs
Exponential backoff for patent APIs is a retry policy that multiplies the wait between successive attempts by a fixed factor, adds randomized jitter to break up synchronized retries, and honors the office’s own Retry-After directive — so a transient 429 or 503 from USPTO, EPO, or WIPO recovers automatically without hammering the endpoint or corrupting a docketed base date.
The mechanism sits inside the polling loop defined by the WIPO API Async Polling Patterns adapter: async job submission returns a 202, and every subsequent status poll must be spaced by a delay this policy computes. Get the policy wrong and the failure is not abstract — a fixed-interval retry storm exhausts a per-key quota during the 18-month publication window, the office rate-limits the whole firm, and a deadline-critical payload silently stalls. This page fixes the exact curve, the status routing that decides what may be retried at all, and the audit record every attempt must leave behind.
Technical Specification: Retry-After, Jitter, and the Backoff Curve
Backoff is not a heuristic you invent; the wire behavior is specified and the offices publish quotas you must respect.
429 Too Many Requestsis defined by RFC 6585 § 4, which states the server may include aRetry-Afterheader indicating how long to wait. When present, that value is authoritative and overrides any computed delay.Retry-Afteris defined by RFC 9110 § 10.2.3 as either an integer number of seconds or an HTTP-date. Both forms appear in practice: USPTO returns delta-seconds on429, while a maintenance window may return an absolute date. A compliant client parses both.- The curve itself is
delay = base * multiplier ** attempt, capped at a ceiling, with jitter applied last. Uncapped exponential growth blocks worker threads indefinitely; jitterless backoff re-synchronizes every worker that failed in the same second (the thundering-herd effect), which is exactly what triggers the next429.
The office quotas the curve must live within are non-negotiable inputs, not tuning knobs: the USPTO Open Data Portal enforces a per-API-key request budget and returns Retry-After on throttle; the EPO Open Patent Services (OPS) fair-use policy meters a weekly volume tier per credential; and WIPO’s PATENTSCOPE surfaces publish asynchronously, so a 404/202 there means “not materialized yet” and must be retried on a slow cadence rather than escalated as an error.
Minimal Reproducible Implementation
The reference policy is a single deterministic function plus a requests session that mounts a urllib3 retry adapter for connection-level failures. It uses Python 3.11+ syntax and never guesses when the office has spoken: an explicit Retry-After always wins over the computed curve.
from __future__ import annotations
import random
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Source: RFC 9110 §10.2.3 (Retry-After) https://www.rfc-editor.org/rfc/rfc9110#field.retry-after
# Source: USPTO Open Data Portal quotas https://developer.uspto.gov/
BASE_DELAY: float = 2.0
MULTIPLIER: float = 2.0
MAX_DELAY: float = 300.0 # 5-minute ceiling; aligns with USPTO/EPO maintenance windows
MAX_RETRIES: int = 5
JITTER_FACTOR: float = 0.5 # full jitter band: ±50% of the capped delay
def compute_delay(attempt: int) -> float:
"""Capped exponential backoff with symmetric jitter. `attempt` is 0-indexed."""
capped = min(BASE_DELAY * (MULTIPLIER ** attempt), MAX_DELAY)
jitter = random.uniform(-JITTER_FACTOR * capped, JITTER_FACTOR * capped)
return max(0.1, capped + jitter) # never sleep to zero — keep a floor
def parse_retry_after(value: str | None) -> float | None:
"""Honor the office's own directive verbatim. Handles delta-seconds and HTTP-date."""
if not value:
return None
if value.isdigit():
return float(value)
when = parsedate_to_datetime(value) # RFC 9110 HTTP-date form
return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
def build_patent_session() -> requests.Session:
"""Session whose adapter retries only transient statuses and respects Retry-After."""
retry = Retry(
total=MAX_RETRIES,
backoff_factor=0.5, # urllib3's own exponential base
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "POST"],
respect_retry_after_header=True, # never override an explicit directive
)
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))
return session
respect_retry_after_header=True is the load-bearing flag: it makes urllib3 defer to a 429/503 directive instead of applying its own curve, and the standalone parse_retry_after helper covers the HTTP-date form for the manual polling loop in the parent adapter, where compute_delay supplies the delay only when the office gave no directive.
Known Gotchas & Compliance Traps
The policy fails in a handful of specific, repeatable ways, and each corrupts docketing state differently.
- Retrying
4xxclient errors. Blindly retrying every non-2xxresponse is the most damaging mistake. A429, 500–504is transient and safe to back off on; a400, 401, 403, 404, 422is a permanent application-layer rejection — a malformed application number, an expired OAuth token, a revoked key, or a withdrawn application. Automated retries on401/403can lock the credential or invalidate the session, converting a fixable error into a firm-wide outage. Route4xxstraight to the validation queue and let the Schema Validation & Error Categorization protocol decide whether it needs a token rotation or a paralegal. - Jitterless or uncapped backoff. Without jitter, every worker that hit the limit at 10:00:00 retries together at 10:00:04, re-triggering the
429. Without a ceiling,2 * 2**10is a 34-minute blocking sleep that silently parks a deadline-critical poll. Apply full jitter and a hardMAX_DELAY. - Ignoring an explicit
Retry-Afterand burning the quota. If the office returnsRetry-After: 120and you compute a 4-second delay instead, you spend the entire window fruitlessly consuming quota. TrackX-RateLimit-Remaining; when it drops below ~5% of the budget, preemptively throttle rather than sprinting into the next429. The office-specific ceilings are catalogued in the EPO Register API Rate Limiting Strategies reference. - Backing off forever with no exit. Retries must terminate. After
MAX_RETRIES, the payload escalates through a deterministic fallback chain — it is never dropped, because a droppedPRIORITY_CRITICALpayload is a missed statutory deadline.
Tiered Fallback and Status Routing
When the curve exhausts, the request escalates through fixed tiers rather than failing open. Patent APIs degrade asymmetrically — a 502 on USPTO Patent Center usually signals a locked publication database, whereas a WIPO 429 typically means IP-based session exhaustion — so the escalation, not the raw status, drives recovery:
- Primary — backoff with jitter (attempts 1–5). Every attempt logs its count, delay, and response headers.
- Secondary — stale cache validation. Serve the last-known-good payload flagged
X-Cache-Stale: trueso the docketing UI can visually mark data a paralegal must not act on blindly. - Tertiary — headless fallback. Only for EPO Register or legacy endpoints returning
503for more than three consecutive cycles, hand off to the EPO Register Headless Browser Fallback adapter within its terms of service. - Quaternary — dead-letter queue. Tag the payload with a priority (
PRIORITY_CRITICALfor 18-month publication anchors,PRIORITY_STANDARDfor fee-status updates) and push to a broker for human intervention.
The routing table that gates all of this is intentionally binary:
| Status | Classification | Action | Compliance implication |
|---|---|---|---|
429, 500–504 |
Transient infrastructure | Backoff + jitter, honor Retry-After |
Safe for automated recovery |
400, 401, 403, 404, 422 |
Permanent application | Halt retries, route to validation queue | Needs token rotation or paralegal review |
A circuit breaker wraps the whole chain: track a sliding-window failure rate and, when 5xx errors exceed ~15% over 60 seconds, open the circuit, bypass retries, and route directly to the dead-letter queue until a recovery timer elapses — this stops a struggling office from consuming the entire worker pool. The escalation logic itself is implemented in the Patent Docket Fallback Routing System.
Integration Point
This policy is one node in the acquisition pipeline, not a standalone client. Upstream, the parent WIPO API Async Polling Patterns adapter owns the POST → 202 → poll → terminal state machine and calls compute_delay (or the parsed Retry-After) between polls; the identical policy protects the synchronous fetches in WIPO Priority Document Sync with Python Requests. Downstream, only a successful 2xx payload proceeds to normalization and the append-only ledger; everything else is quarantined or dead-lettered, keeping the Patent Office Portal Sync & Data Ingestion layer deterministic.
Every attempt must leave an immutable audit record so a compliance reviewer can reconstruct exactly what the office returned and when. Each record carries request_id, jurisdiction_code, attempt_number, delay_applied, jitter_offset, retry_after_header_value, fallback_tier_triggered, dlq_priority_tag, and an ISO-8601 timestamp_utc, written to write-once storage governed by the Security & Access Control Boundaries model. When a PRIORITY_CRITICAL payload enters the dead-letter queue, the system computes the remaining statutory window and escalates to the docketing calendar via webhook — automated retry may never silently drop a deadline. Keep BASE_DELAY, MAX_DELAY, and MAX_RETRIES in environment variables so they can be widened during an unexpected portal maintenance window without a redeploy.
Frequently Asked Questions
Should a computed backoff delay ever override the office's Retry-After header?
Retry-After value, that directive is authoritative under RFC 9110 and must be honored verbatim. The computed exponential curve applies only when the office gives no directive. Setting respect_retry_after_header=True on the urllib3 Retry object enforces this at the adapter level.
Why must 4xx responses bypass the retry loop entirely?
4xx is a permanent application-layer rejection — a malformed payload, an expired token, a revoked key, or a withdrawn application. Retrying it wastes quota and, on 401/403, can lock the credential or invalidate the session. Only 429 and 500–504 are transient and safe to back off on; everything else routes to the validation queue.
What does jitter actually prevent in a patent-docketing context?
What happens to a payload after MAX_RETRIES is exhausted?
PRIORITY_CRITICAL payload (for example an 18-month publication anchor) triggers a webhook that recomputes the remaining statutory window and alerts the docketing calendar.
Related
- ← WIPO API Async Polling Patterns — the parent async job lifecycle this backoff policy is called from.
- WIPO Priority Document Sync with Python Requests — the sibling fetch flow that reuses the same retry policy.
- Schema Validation & Error Categorization — where routed
4xxresponses are triaged. - EPO Register API Rate Limiting Strategies — office-specific quota ceilings the curve lives within.