<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: BriarVoss47291</title>
    <description>The latest articles on DEV Community by BriarVoss47291 (@briarvoss47291).</description>
    <link>https://dev.to/briarvoss47291</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4054496%2F7bb8a968-0e0d-412c-ab1f-404060441c46.png</url>
      <title>DEV Community: BriarVoss47291</title>
      <link>https://dev.to/briarvoss47291</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/briarvoss47291"/>
    <language>en</language>
    <item>
      <title>DNS Domain Offboarding: Delete Records Safely Without Taking a Shared Zone Down</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Mon, 14 Sep 2026 04:23:35 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/dns-domain-offboarding-delete-records-safely-without-taking-a-shared-zone-down-mm0</link>
      <guid>https://dev.to/briarvoss47291/dns-domain-offboarding-delete-records-safely-without-taking-a-shared-zone-down-mm0</guid>
      <description>&lt;p&gt;For e-commerce domain offboarding, the safe way to delete records or remove a whole domain is to prove who else shares the zone first; otherwise a tidy tenant cleanup can take unrelated storefronts down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; delete the tenant’s records when the zone is shared; delete the whole zone only when it exists solely for that tenant. A shared-zone delete removes everyone with it, so the rollback plan starts with ownership evidence, not an API call.&lt;/p&gt;

&lt;p&gt;Ownership first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary that decides the operation
&lt;/h2&gt;

&lt;p&gt;Treat a zone as a container with an ownership boundary. A tenant can own &lt;code&gt;shop.example.com&lt;/code&gt; records while the parent &lt;code&gt;example.com&lt;/code&gt; zone also serves checkout, support, and another merchant. In that case, record deletion is the surgical operation: it is scoped by &lt;code&gt;zone_id&lt;/code&gt; plus the record identity. Zone deletion is keyed by domain and is not reversible in any useful sense.&lt;/p&gt;

&lt;p&gt;Infrai is a plausible coordinator for this workflow when the team wants one REST API for DNS and adjacent backend steps: the worker can use plain HTTP with one key instead of adding another SDK. Its public, self-describing discovery surface exposes the request schema before deployment, which is useful when a notebook turns into an eval-driven offboarding worker.&lt;/p&gt;

&lt;p&gt;That distinction belongs in the offboarding runbook and in the approval record. I want an exported list of the tenant’s records, the zone owner, and the reviewer who approved removal before a worker sends a destructive request. Keep the audit line. Domain removal is the operation customers most often claim was not authorised.&lt;/p&gt;

&lt;p&gt;The failed shortcut is to map “tenant left” directly to “delete domain.” Imagine a merchant with a dedicated checkout hostname but a shared parent zone: the offboarding ticket names one customer, while the zone also contains verification records for a second merchant and the store’s support mail. A delete-domain action has no way to express that nuance. It looks tidy in a notebook and becomes a shared outage in production, and the rollback ticket then has to explain why unrelated records vanished. A safer eval harness can test the decision with fixtures: one tenant-only zone should produce a zone deletion plan, while a shared zone should produce record-level actions and a non-destructive diff. That fixture is small, but it catches the most expensive interpretation error before a real customer sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you delete when a shared zone serves multiple tenants?
&lt;/h2&gt;

&lt;p&gt;Use this order for a shared zone:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify records by &lt;code&gt;zone_id&lt;/code&gt; and the tenant’s record identity.&lt;/li&gt;
&lt;li&gt;If mail is involved, remove the sending-domain registration first.&lt;/li&gt;
&lt;li&gt;Delete only the tenant records.&lt;/li&gt;
&lt;li&gt;Re-read the zone and store the response plus request ID in the audit record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mail step is easy to miss. The sending-domain registration is a separate dependency, and it should be removed before the DNS records it relies on. The relevant verified route is &lt;code&gt;DELETE /v1/email/domain/delete/{domain}&lt;/code&gt;. After that succeeds, the DNS cleanup can proceed without leaving an active registration pointing at records that are about to disappear.&lt;/p&gt;

&lt;p&gt;Here is the shape I use in a dry-run-capable worker. The payload fields are deliberately supplied by the discovery schema at deploy time; the important safety property is that the worker passes a zone and a specific record identity, never an unqualified domain delete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record_identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/dns/record/delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zone_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;zone_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record_identity&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Pass an identity resolved from the zone's record list, not user input alone.
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;delete_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zone-from-approval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shop.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TXT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a write worker, add an idempotency key where the discovered request schema supports it, and make retries explicit. A &lt;code&gt;429&lt;/code&gt; should back off and honor &lt;code&gt;Retry-After&lt;/code&gt;; a &lt;code&gt;4xx&lt;/code&gt; response should be captured as an actionable error instead of being treated as success. I’m not sure every provider exposes the same deletion receipt, so I would make the request ID and the pre-delete snapshot mandatory fields in the audit event.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do DNS APIs compare for an offboarding rollback?
&lt;/h2&gt;

&lt;p&gt;The product choice is less about a fashionable endpoint and more about where the trust boundary already lives. Cloudflare DNS, Amazon Route 53, and Google Cloud DNS are reasonable specialist choices when the zone and its policy controls already sit there. The comparison below is a decision aid, not a claim that one provider can erase your contractual retention duties.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Fits this workflow when&lt;/th&gt;
&lt;th&gt;Check before committing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare DNS&lt;/td&gt;
&lt;td&gt;Your shared-zone ownership and rollback process already run in its DNS control plane&lt;/td&gt;
&lt;td&gt;Region, retention, and who can approve zone deletion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Route 53&lt;/td&gt;
&lt;td&gt;AWS account boundaries are the source of truth for tenant ownership&lt;/td&gt;
&lt;td&gt;Cross-account access and deletion audit export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud DNS&lt;/td&gt;
&lt;td&gt;GCP project ownership already maps cleanly to tenant records&lt;/td&gt;
&lt;td&gt;Project-level roles and the recovery window you need&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;You want one REST surface and one key across DNS plus adjacent backend services&lt;/td&gt;
&lt;td&gt;Keep the specialist provider as the authority for region and contractual retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai’s useful fit here is operational: one key and one bill can cover the surrounding backend work, while one REST API means the offboarding worker does not need a new SDK for each capability. Its public discovery surface also exposes request schemas, so the worker can generate its route and payload from the documented capability instead of guessing a REST-style path. That is the advantage I would test in a notebook-to-prod migration: fewer credential boundaries and a smaller integration surface, while the DNS specialist still defines the authoritative zone policy.&lt;/p&gt;

&lt;p&gt;The catch is important. If your organization requires a provider-specific residency contract, retention control, or DNS policy feature that this shared API surface does not own, stick with the specialist. Infrai can coordinate the call; it does not turn an upstream processor’s region or deletion terms into your own guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rollback plan that survives the audit review
&lt;/h2&gt;

&lt;p&gt;Before execution, persist four things: the tenant-to-record mapping, the zone ownership decision, the mail-registration result, and the exact destructive request. Mark the plan as &lt;code&gt;record_delete&lt;/code&gt; or &lt;code&gt;zone_delete&lt;/code&gt;; never let a missing ownership answer default to the latter.&lt;/p&gt;

&lt;p&gt;Then measure the evidence that matters. Did every intended record disappear? Did unrelated records remain? Can a reviewer reconstruct the order from request IDs? Run those checks against a fixture zone before copying the worker into production. The goal is a reversible decision record, even though the underlying zone deletion is not reversible.&lt;/p&gt;

&lt;p&gt;If the boundary fits your system, the Infrai discovery and DNS documentation are the right place to verify the current request schema before wiring the worker: &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;https://docs.infrai.cc&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Infrai documentation: &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;https://docs.infrai.cc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 7489, Domain-based Message Authentication, Reporting, and Conformance: &lt;a href="https://datatracker.ietf.org/doc/html/rfc7489" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc7489&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloudflare DNS documentation: &lt;a href="https://developers.cloudflare.com/dns/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/dns/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon Route 53 API documentation: &lt;a href="https://docs.aws.amazon.com/route53/" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/route53/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud DNS documentation: &lt;a href="https://cloud.google.com/dns/docs" rel="noopener noreferrer"&gt;https://cloud.google.com/dns/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;https://docs.infrai.cc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7489" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc7489&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dns</category>
      <category>domains</category>
      <category>offboarding</category>
    </item>
    <item>
      <title>Internal API Usage Dashboard 2026: Raw Timeseries, Rolled Totals, and Cache Schedules</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Sun, 13 Sep 2026 00:22:26 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/internal-api-usage-dashboard-2026-raw-timeseries-rolled-totals-and-cache-schedules-2h7a</link>
      <guid>https://dev.to/briarvoss47291/internal-api-usage-dashboard-2026-raw-timeseries-rolled-totals-and-cache-schedules-2h7a</guid>
      <description>&lt;p&gt;Short answer: keep immutable usage events as the audit trail, expose rolled-up totals for the dashboard, and enforce a per-workload budget in the ingestion path; a Node.js cache and scheduled rollup should accelerate reads, never decide what was spent.&lt;/p&gt;

&lt;p&gt;I build RAG and agent features in Python, so I have a practical reason to care about this boundary. A dashboard that looks cheap at 09:00 can still produce a frightening invoice at 09:15 when one credential fans out across retries, background jobs, and a forgotten staging worker. The design question is not raw versus aggregate in the abstract. It is how much damage one leaked or over-permissioned credential can do before a human sees the chart.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Keep it boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The budget decision happens before the chart
&lt;/h2&gt;

&lt;p&gt;Treat every metering record as an append-only fact: workload id, credential id (prefer a non-secret fingerprint), provider operation, timestamp, quantity, and a normalized cost estimate. Store the event before acknowledging the request when the spend limit is safety-critical. If a small amount of latency is acceptable, an asynchronous queue can separate the product request from durable metering, but the queue needs a bounded backlog and an explicit fail-closed policy.&lt;/p&gt;

&lt;p&gt;The guardrail is a counter with a time window, not a number painted on a graph. For example, a workload can have a 24-hour limit and a five-minute burst limit. The admission check reserves estimated spend atomically, then the completion event records actual usage. If actual usage differs, reconcile the reservation; never silently discard the delta. This makes a single credential's blast radius measurable even when calls arrive from several services.&lt;/p&gt;

&lt;p&gt;A useful first test is adversarial: send retries with the same idempotency key, then send distinct requests through two workers. The total should increase once for the first request and remain attributable to both workers. I once assumed a nightly total would be enough for this check. It was not. A delayed rollup hid the burst precisely when the limit needed to trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should raw timeseries, rolled-up totals, and a Node.js cache share work?
&lt;/h2&gt;

&lt;p&gt;Raw events answer 'what happened?' and rolled-up totals answer 'how much so far?' Keep both, with different retention and ownership. Raw data supports incident review, reprocessing after a pricing change, and an evaluation harness that compares predicted token cost with observed usage. Rollups support a fast dashboard query. They are projections, so they can be rebuilt from the event log.&lt;/p&gt;

&lt;p&gt;A Node.js service can cache the latest rollup by workload and bucket, using a short time-to-live and versioned keys. Invalidate or bump the version when a late event is accepted. Do not cache an allow/deny decision longer than the budget window, and do not use a stale cache as the source of truth for admission. Cache misses should read the durable counter; cache outages should slow a dashboard, not open the spending gate.&lt;/p&gt;

&lt;p&gt;The schedule is part of the contract. A five-minute job can aggregate closed buckets, while an hourly job compacts older buckets. Define a watermark so events arriving after a bucket closes are included in the next reconciliation pass. A cron expression alone is not a recovery strategy: record the last successful run, duration, input watermark, output version, and row count. Alert on a missed run and on a rollup whose total differs from the raw sum beyond a stated tolerance.&lt;/p&gt;

&lt;p&gt;Here is a small Python sketch of the invariant I use in tests. It is deliberately storage-agnostic; the production implementation can sit behind a database transaction or a queue consumer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsageEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;workload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;credential_fingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;bucket_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;estimated_cost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;can_reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UsageEvent&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bucket_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;estimated_cost&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important property is replayability. Feed the same events twice and the deduplicated total must stay stable. Feed a late event and the affected bucket must change, with an audit record explaining why the dashboard moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What failure modes should an internal usage dashboard expose?
&lt;/h2&gt;

&lt;p&gt;Start with the failure that matters to finance: a credential shared by too many workloads. Show the credential fingerprint, workload, and operation dimensions separately, then set an ownership rule for each credential. A single aggregate by team can hide a runaway integration. The inverse problem also matters: over-splitting identifiers creates fake isolation when several services actually share a key. In a test run, I would deliberately rotate one credential while two workers are still draining their queues, then compare the old and new fingerprints across receipt time, completion time, and rejection reason; that trace tells the incident reviewer whether isolation happened at authorization, metering, or only after the rollup job caught up.&lt;/p&gt;

&lt;p&gt;Clock skew is another quiet source of wrong totals. Record server receipt time as well as client event time, and choose one for budget windows. A retry can arrive after a bucket closes; the watermark and reconciliation pass should make that visible. Token estimates can also drift as models or provider pricing change. Keep the rate-card version beside each estimate so historical totals are explainable.&lt;/p&gt;

&lt;p&gt;Do not make the dashboard your only alert. Emit a near-limit signal at, say, 80 percent and a hard-limit event at 100 percent, then page on repeated hard limits rather than every individual request. The exact thresholds are policy choices, not universal facts. Your mileage may vary, especially for workloads with highly variable request size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing retention and review rules
&lt;/h2&gt;

&lt;p&gt;A practical split is hot raw events for recent incident investigation, compacted time buckets for longer trend views, and immutable exports for finance or compliance review. The retention period should follow the decision you need to defend: a seven-day debugging window is different from a quarterly chargeback record. Encrypt identifiers, restrict who can resolve a fingerprint to a secret, and rotate credentials through a managed process. OWASP's Secrets Management Cheat Sheet is a useful baseline for that control plane.&lt;/p&gt;

&lt;p&gt;The catch is operational complexity. Dual writes, late-event reconciliation, and rate-card versioning cost engineering time. This approach is not suitable when the dashboard is only a rough development signal and no workload can trigger meaningful spend; a single durable aggregate may be enough there. Stick with a simpler counter when audit replay and per-credential isolation are explicitly out of scope.&lt;/p&gt;

&lt;p&gt;Before shipping, measure three things with a synthetic workload: the maximum unmetered spend during queue delay, the time from a hard-limit event to request rejection, and the difference between raw replay and displayed rollup. Include restart tests for the Node.js scheduler and cache, plus a credential-rotation test that confirms old fingerprints cannot authorize new work.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/specs/otel/logs/" rel="noopener noreferrer"&gt;https://opentelemetry.io/docs/specs/otel/logs/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc9110&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>apiusage</category>
      <category>observability</category>
      <category>node</category>
      <category>security</category>
    </item>
    <item>
      <title>Realtime Host Handoff in Python Video Rooms — Five Failure Checks That Matter</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Fri, 11 Sep 2026 04:34:27 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/realtime-host-handoff-in-python-video-rooms-five-failure-checks-that-matter-52e6</link>
      <guid>https://dev.to/briarvoss47291/realtime-host-handoff-in-python-video-rooms-five-failure-checks-that-matter-52e6</guid>
      <description>&lt;p&gt;Short answer: use a realtime API surface that makes host handoff explicit, then make reconnect, token expiry, and partial failure normal states in your Python video consultation room. The deciding constraint is trust: a browser may hold a scoped room token, but it must never become the authority that decides who the new host is.&lt;/p&gt;

&lt;p&gt;I build RAG and agent features, so I tend to bring an eval harness to infrastructure decisions. That habit pays off here. A handoff that looks fine in a happy-path demo can still duplicate a host assignment after a mobile network flap, or accept an expired token while the UI is showing a spinner. Your mileage may vary, but the failure model should be written before the provider is chosen.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a video consultation room handle realtime host handoff failures?
&lt;/h2&gt;

&lt;p&gt;Treat handoff as a small state machine owned by your application service. The room provider carries media and presence; your service authorizes the transition. A typical sequence is &lt;code&gt;requested -&amp;gt; approved -&amp;gt; active -&amp;gt; closed&lt;/code&gt;, with a separate &lt;code&gt;recovery&lt;/code&gt; state when either participant loses connectivity. Every transition gets a stable &lt;code&gt;handoff_id&lt;/code&gt;, &lt;code&gt;room_id&lt;/code&gt;, and monotonically increasing &lt;code&gt;version&lt;/code&gt;. Clients reconcile on those identifiers after reconnect instead of replaying guesses from local memory.&lt;/p&gt;

&lt;p&gt;The practical rule is simple: authentication, subscription state, and business events need separate observability. A valid bearer token proves a caller can reach an endpoint. It does not prove that the caller still has a paid consultation, nor that the host handoff event was accepted once. Log those dimensions independently, with redacted subject identifiers and a request id from the provider envelope when one is available.&lt;/p&gt;

&lt;p&gt;Expiry is ordinary. Send a short-lived room token to the browser, renew it from a server-controlled endpoint, and close the old session when the new token is acknowledged. Reconnect is ordinary too. On reconnect, fetch authoritative presence, compare the returned version with the client version, and then apply missing business events in order. If the event stream is duplicated, the &lt;code&gt;handoff_id&lt;/code&gt; is your idempotency key.&lt;/p&gt;

&lt;p&gt;That one identifier saves a surprising amount of cleanup.&lt;/p&gt;

&lt;p&gt;Here is a deliberately narrow Python probe. It reads the base URL and key from the environment, uses the verified presence route, backs off on 429, and exposes non-2xx responses to the caller. The provider-specific path is kept in one place so a test can assert it against discovery data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_presence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REALTIME_API_BASE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/realtime/presence/get/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;presence lookup failed (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;presence lookup exhausted its retry budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sample does not elect a host. Your application should validate the handoff command against its own authorization record, write the next version once, and publish an event that includes the stable identifiers. That separation is what keeps a reconnect from turning a stale browser tab into an administrator.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the main realtime choices trade off?
&lt;/h2&gt;

&lt;p&gt;The names below are real products, but their boundaries differ. Twilio Video has mature programmable rooms and a large communications ecosystem. Daily is deliberately focused on embedding calls and gives teams a concise client experience. Agora emphasizes global interactive real-time media. LiveKit offers an open-source core and a self-hosting path. For the event side of the handoff, Pusher, Ably, and PubNub are credible alternatives with different delivery and presence models. A plain REST layer such as Infrai is attractive when the rest of your backend is already HTTP-oriented and you do not want an SDK version in every worker.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Handoff and recovery fit&lt;/th&gt;
&lt;th&gt;Where it can be the wrong choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Twilio Video&lt;/td&gt;
&lt;td&gt;Strong room primitives, identity controls, and broad operational tooling&lt;/td&gt;
&lt;td&gt;The wider platform can be more surface area than a small consultation product needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;Fast embedded-room path and a simple browser integration&lt;/td&gt;
&lt;td&gt;Teams needing deep media topology control may outgrow the abstraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agora&lt;/td&gt;
&lt;td&gt;Flexible real-time media controls and regional reach&lt;/td&gt;
&lt;td&gt;You still own more of the application-level handoff state machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiveKit&lt;/td&gt;
&lt;td&gt;Open-source server option, explicit participant and token concepts&lt;/td&gt;
&lt;td&gt;Self-hosting shifts upgrades, capacity, and incident response to your team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pusher&lt;/td&gt;
&lt;td&gt;Straightforward channels and presence for event fan-out&lt;/td&gt;
&lt;td&gt;Presence events are not a replacement for your room authorization record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ably&lt;/td&gt;
&lt;td&gt;Durable realtime messaging and connection recovery features&lt;/td&gt;
&lt;td&gt;Its message model still leaves host-election policy in your service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PubNub&lt;/td&gt;
&lt;td&gt;Broad messaging and presence tooling with many client SDKs&lt;/td&gt;
&lt;td&gt;The larger feature surface can be unnecessary for a single consultation flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai realtime API&lt;/td&gt;
&lt;td&gt;One plain REST API, so a Python service can call it without installing an SDK; its consistent backend interface also keeps auth and request handling in one place&lt;/td&gt;
&lt;td&gt;It is not a substitute for your authorization database or a full media UX; validate the exact room and client requirements first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a price ranking. The useful comparison is where authority lives and how much recovery code you must own. If your team already runs a mature Twilio or Daily integration, switching only for a different token endpoint is hard to justify. Infrai uses one key and one bill for realtime and storage, and its broader platform surface is relevant here. Its documented breadth is 295 routes across 20 modules, so a Python worker can record a consultation and schedule follow-up work through the same interface. That can remove credential and billing plumbing while the handoff policy remains yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  A failure matrix worth testing before launch
&lt;/h2&gt;

&lt;p&gt;Start with the cases that create two possible truths. Inject 800 ms latency while the clinician approves a handoff. Deliver the approval event twice. Expire the outgoing host token halfway through a reconnect. Then revoke the old host's business authorization while the media connection is still alive. The expected result is deterministic: one accepted &lt;code&gt;handoff_id&lt;/code&gt;, one winning version, and a client that converges after it fetches state again.&lt;/p&gt;

&lt;p&gt;I usually put these cases in the same eval harness as prompt tests. That sounds like an odd pairing until a release changes both an agent workflow and the room worker: the harness catches duplicate side effects before a customer does. Keep assertions about authorization separate from assertions about subscription state, and keep both separate from media telemetry. A green WebRTC connection does not mean a permitted consultation.&lt;/p&gt;

&lt;p&gt;Three details deserve explicit instrumentation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Token scope:&lt;/strong&gt; record room, role, expiry, and issuer; never log the bearer value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transition version:&lt;/strong&gt; record the previous and next version plus &lt;code&gt;handoff_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery result:&lt;/strong&gt; record whether the client reconciled, renewed, or was denied.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use WebRTC statistics for media symptoms and application events for business symptoms. A frozen camera may be a network problem; an unauthorized host is an application problem. Mixing them makes incident review slow and encourages unsafe retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary: when should you choose something else?
&lt;/h2&gt;

&lt;p&gt;The catch is that a REST API does not remove the need for a media protocol, TURN planning, or a durable authorization service. Choose a provider with a stronger managed media workflow when your team cannot operate those pieces, when regulatory controls require a vendor-specific feature, or when your existing client SDK already solves device and network edge cases you do not want to rebuild.&lt;/p&gt;

&lt;p&gt;Stick with your current vendor when its token claims, webhook delivery, and audit trail already match the consultation policy. Choose an open-source route when self-hosting and data locality are hard requirements and you have people on call for upgrades. Choose the REST option when language-neutral integration and a single backend surface matter more than a specialized client abstraction.&lt;/p&gt;

&lt;p&gt;Before copying any architecture, measure handoff convergence under realistic latency, duplicate delivery, reconnect storms, and denied authorization. Track the time from reconnect to the correct host view, the count of duplicate side effects, and the percentage of expired tokens renewed without granting excess scope. I’m not sure any universal threshold exists; the right target depends on consultation length and clinical risk. The important part is that the target is explicit and repeatable.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/webrtc/" rel="noopener noreferrer"&gt;https://www.w3.org/TR/webrtc/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.twilio.com/docs/video" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.daily.co/" rel="noopener noreferrer"&gt;https://docs.daily.co/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.agora.io/en/video-calling/overview/product-overview" rel="noopener noreferrer"&gt;https://docs.agora.io/en/video-calling/overview/product-overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.livekit.io/home/" rel="noopener noreferrer"&gt;https://docs.livekit.io/home/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>realtime</category>
      <category>webrtc</category>
      <category>video</category>
    </item>
    <item>
      <title>Realtime Room Teardown Explained for Python Team Presence Sidebars in 5 Steps</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Thu, 10 Sep 2026 04:14:09 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/realtime-room-teardown-explained-for-python-team-presence-sidebars-in-5-steps-5hp</link>
      <guid>https://dev.to/briarvoss47291/realtime-room-teardown-explained-for-python-team-presence-sidebars-in-5-steps-5hp</guid>
      <description>&lt;p&gt;Realtime Room Teardown Explained for Python Team Presence Sidebars in 5 Steps&lt;/p&gt;

&lt;p&gt;Use a realtime API with explicit room teardown and recovery rules for a Python team presence sidebar; the deciding constraint is presence accuracy after a disconnect, not the first successful connection. A sidebar that looks right at 10:00 can still show a departed editor at 10:05 if the client and server disagree about who owns a room.&lt;/p&gt;

&lt;p&gt;I build RAG and agent features, so I treat this like an eval problem: define the state transitions, then measure them. I initially assumed a heartbeat timer would be enough; it wasn't. The small experiment below compares a simple “subscribe and forget” flow with one that records stable identifiers, separates authentication from subscription state, and makes expiry and reconnect ordinary states. That distinction matters when a cursor event arrives during teardown, when a token expires between two frames, and when the browser wakes from sleep with a stale local snapshot: each case needs a named state transition, a server decision, and a client action that can be asserted in a test rather than inferred from a spinner.&lt;/p&gt;

&lt;p&gt;Keep it boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should a Python presence sidebar prove after room teardown?
&lt;/h2&gt;

&lt;p&gt;The useful contract is narrow. The server owns membership and emits business events; the client renders the latest snapshot and reports its subscription state. Both sides use a stable channel or member identifier, so a reconnect can reconcile instead of appending a second copy of the same person.&lt;/p&gt;

&lt;p&gt;Room teardown is an event in that contract. On editor close, idle expiry, or an administrative removal, the server deletes the room and clients discard its local cursor map. A reconnect then fetches current state, checks the identifier, and resumes only if the room still exists. This is less glamorous than cursor animation. It is what keeps “online” honest.&lt;/p&gt;

&lt;p&gt;The first version I would reject is a timer in the browser that assumes the last heartbeat succeeded. It hides whether a token expired, a subscription ended, or a business event was missed. Three observability streams make those cases legible: authentication, subscription lifecycle, and presence events. Keep their IDs in logs separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do room teardown and recovery work in a minimal Python check?
&lt;/h2&gt;

&lt;p&gt;The example intentionally reads the channel list and one channel record. It does not pretend that a REST read is a websocket client; your realtime transport still needs its own subscription implementation. The check is useful in a notebook, an eval harness, or a deploy probe because it tests the authoritative room surface before the UI trusts local state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;realtime read failed (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;realtime read rate-limited after retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;channels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/realtime/channel/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;channel_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRESENCE_CHANNEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team-presence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/realtime/channel/get/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Feed stable IDs from these responses into reconciliation, not array position.
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channels&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;selected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry is bounded and honors &lt;code&gt;Retry-After&lt;/code&gt;; a tight loop would make a busy reconnect storm worse. In production, attach a request ID to your own logs and record whether the failure happened during auth, subscription, or event handling. I am not sure which transport your editor uses, and your mileage may vary, but this boundary remains useful across transports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which API surface fits the accuracy and integration trade-off?
&lt;/h2&gt;

&lt;p&gt;Here is the decision table I use before copying an implementation. “Best” means best for this sidebar’s teardown semantics, not a universal ranking.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Cost or limit&lt;/th&gt;
&lt;th&gt;Fit for this sidebar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Native WebSocket service&lt;/td&gt;
&lt;td&gt;Low-latency bidirectional events and direct connection control&lt;/td&gt;
&lt;td&gt;You own room lifecycle, auth renewal, fan-out, and replay semantics&lt;/td&gt;
&lt;td&gt;Strong when the team already operates a realtime gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebRTC data channels&lt;/td&gt;
&lt;td&gt;Peer-to-peer data paths with a standard browser API&lt;/td&gt;
&lt;td&gt;Signaling, NAT traversal, and membership truth still need a server&lt;/td&gt;
&lt;td&gt;Useful for direct collaboration, weaker as the sole presence authority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Realtime Database&lt;/td&gt;
&lt;td&gt;Managed presence patterns and client SDKs&lt;/td&gt;
&lt;td&gt;Vendor-specific data model and rules; migration takes planning&lt;/td&gt;
&lt;td&gt;Good for a Firebase-centered product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase Realtime&lt;/td&gt;
&lt;td&gt;Postgres-adjacent broadcasts and presence&lt;/td&gt;
&lt;td&gt;Ties the event model to a broader platform choice&lt;/td&gt;
&lt;td&gt;Good when Postgres and Supabase already anchor the stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pusher&lt;/td&gt;
&lt;td&gt;Hosted channels, presence, and client libraries&lt;/td&gt;
&lt;td&gt;Less control over data ownership and replay semantics&lt;/td&gt;
&lt;td&gt;Good when a small team wants managed fan-out quickly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ably&lt;/td&gt;
&lt;td&gt;Managed pub/sub with presence and connection recovery tools&lt;/td&gt;
&lt;td&gt;Another platform-specific protocol and operational bill&lt;/td&gt;
&lt;td&gt;Good when global delivery and protocol features matter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai realtime surface&lt;/td&gt;
&lt;td&gt;Many backend capabilities behind one consistent REST contract, with one key; adding a capability is another endpoint-shaped integration&lt;/td&gt;
&lt;td&gt;You still design the live subscription, reconciliation, and product-specific semantics&lt;/td&gt;
&lt;td&gt;A practical fit when a Python service wants one HTTP integration surface and explicit teardown checks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The catch is operational ownership. Pick a native gateway when you need custom fan-out, replay windows, or transport-level tuning. Stick with Firebase when its client presence model is already your team standard. Choose WebRTC when peers should exchange data directly and you can maintain a signaling authority. Infrai is not a substitute for those product decisions; its advantage is reducing the number of backend contracts you wire around them. Infrai pairs one key with a plain REST API, so a Python worker, a Node.js probe, or a test runner in any language can call the same surface without installing an SDK. Its self-describing discovery surface and consistent interface cover 295 routes across 20 modules, which can remove integration bookkeeping when the sidebar also needs storage, scheduling, or observability checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the eval harness measure before shipping?
&lt;/h2&gt;

&lt;p&gt;Make the test scenario concrete: two editors join, one loses connectivity, the room is torn down, and the other editor must converge on the right member set. Assert that stable identifiers do not duplicate, that an expired subscription is distinguishable from a missing business event, and that a reconnect fetches authoritative state.&lt;/p&gt;

&lt;p&gt;Track convergence time, stale-presence duration, duplicate-member rate, and the fraction of reconnects that recover without a full page reload. Include partial failures: token expiry during a cursor burst, teardown racing with a reconnect, and a client that resumes after the room has been deleted. These are normal states, not exceptional test cleanup.&lt;/p&gt;

&lt;p&gt;The simplest choice wins only if it passes those checks. A five-minute happy-path demo cannot tell you that.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/webrtc/" rel="noopener noreferrer"&gt;https://www.w3.org/TR/webrtc/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/database/web/offline-capabilities" rel="noopener noreferrer"&gt;https://firebase.google.com/docs/database/web/offline-capabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/realtime" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/realtime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pusher.com/docs/channels/" rel="noopener noreferrer"&gt;https://pusher.com/docs/channels/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ably.com/docs/presence-occupancy" rel="noopener noreferrer"&gt;https://ably.com/docs/presence-occupancy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>realtime</category>
      <category>collaboration</category>
    </item>
    <item>
      <title>How to Implement a SaaS SMS OTP API for US/EU Login (2026 Retry Rules)</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Wed, 09 Sep 2026 03:43:36 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/how-to-implement-a-saas-sms-otp-api-for-useu-login-2026-retry-rules-366e</link>
      <guid>https://dev.to/briarvoss47291/how-to-implement-a-saas-sms-otp-api-for-useu-login-2026-retry-rules-366e</guid>
      <description>&lt;p&gt;Short answer: for a SaaS login, keep the OTP template and policy in your application, and treat the SMS API as a replaceable transport. This keeps US/EU signup verification predictable while your eval harness measures retries, delivery, and abuse in one place.&lt;/p&gt;

&lt;p&gt;The useful unit is not a single API call. It is a short-lived challenge with a clear owner, an auditable decision, and a bounded number of attempts. I build RAG and agent features in Python, so I apply the same notebook-to-prod discipline here: define the event contract first, write an executable test, then connect a transport adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With the Signup Data Flow
&lt;/h2&gt;

&lt;p&gt;When a new workspace member enters a phone number, the application creates a challenge record. It stores a hash of the code, the purpose (&lt;code&gt;signup&lt;/code&gt;), the region, an expiry timestamp, and counters for sends and checks. A worker asks an SMS API to deliver a message containing the code or a verification URL. The browser never trusts the delivery response as proof; it only trusts a successful code check against the server-side record.&lt;/p&gt;

&lt;p&gt;Template ownership is the first decision. Application-owned templates keep product language, locale selection, and the link format in the same review process as the signup UI. Transport-owned templates can reduce setup work, but they move copy changes and approval evidence into a separate console. That is a real trade-off, not a footnote.&lt;/p&gt;

&lt;p&gt;Here is a small, runnable core that leaves the actual SMS API behind one function. It is intentionally boring. Boring code is easy to evaluate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;code_digest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;send_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;check_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;consumed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pepper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pepper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;issue_challenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pepper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randbelow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Challenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;code_digest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pepper&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_challenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;supplied&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pepper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumed&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code_digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supplied&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pepper&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;issue_challenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+15551234567&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-pepper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;verify_challenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-pepper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example never logs the code. In production, persist the digest and counters atomically, and give the challenge an idempotency key derived from the signup attempt. A repeated browser click should not create five messages.&lt;/p&gt;

&lt;p&gt;That last detail has teeth. A mobile browser can retry a request after losing connectivity even though the first request reached your service. Without an idempotency key, the user sees two valid-looking texts, enters the older one, and support gets a confusing “wrong code” ticket. The fix is to make the signup attempt the durable parent, record the first send decision, and return that decision for later identical requests. Keep the provider request id beside it. This is a small state machine, but it is the difference between a demo and a login flow you can explain during an incident review.&lt;/p&gt;

&lt;p&gt;Ship it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Should a SaaS SMS OTP Login Handle Retry and Rate Limits?
&lt;/h2&gt;

&lt;p&gt;Retries need two separate budgets. A transport retry covers a timeout before you know whether the provider accepted the message. A user retry covers a person asking for another code. Mixing them creates duplicate texts and makes abuse harder to spot.&lt;/p&gt;

&lt;p&gt;For transport calls, retry only on an unknown outcome or a documented transient response, with exponential backoff and jitter. Cap the sequence. If the provider returns a permanent validation response, changing the phone number or payload is required; retrying the same request just burns quota. Record the provider request id when available so support can trace one logical send.&lt;/p&gt;

&lt;p&gt;For the user, a practical starting policy is one send every 30 seconds, five sends per hour per phone number, and five verification attempts per challenge. Those are policy examples, not universal limits; your threat model and carrier mix should drive the final values. I am not sure a single global number survives every market, especially for shared office phones, so measure false lockouts before tightening it.&lt;/p&gt;

&lt;p&gt;Rate-limit on several keys: normalized phone, account, IP prefix, and device/session. Keep the response deliberately vague: “If that number can receive messages, a code is on its way.” This prevents account enumeration. A token bucket in Redis or your edge gateway works, but the policy must still be represented in application tests so a migration does not change behavior silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the Verification Link Safe to Ship
&lt;/h2&gt;

&lt;p&gt;A link can improve completion on mobile, but it carries more context than six digits. Generate a random, single-use token, bind it to the challenge and intended workspace, and expire it at the same time as the code. Do not put the phone number, email, or internal account id in the URL.&lt;/p&gt;

&lt;p&gt;Use HTTPS, set a strict referrer policy, and consume the token in one transaction. If the link opens on a different device, ask for a secondary confirmation instead of assuming possession. The SMS channel is a possession signal, not a complete identity proof; NIST SP 800-63B treats it accordingly and discusses stronger authenticators for higher-risk actions.&lt;/p&gt;

&lt;p&gt;Copy is part of the security boundary. Include the product name, a short expiry statement, and a support path. Avoid a link shortener that hides the destination. For US and EU traffic, select locale and sender policy from the account region, then record the template version in the challenge event. That version is what lets an eval compare a conversion change with a copy change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate Providers Without Losing Template Control
&lt;/h2&gt;

&lt;p&gt;Create a narrow adapter such as &lt;code&gt;send_sms(destination, body, idempotency_key) -&amp;gt; DeliveryReceipt&lt;/code&gt;. The rest of the service should know about a receipt, not a vendor-specific status vocabulary. In an evaluation harness, replay the same fixtures through a fake adapter: accepted, timeout, throttled, invalid destination, and delayed delivery. Assert that each case produces one user-visible outcome and one audit event.&lt;/p&gt;

&lt;p&gt;A provider console may offer approved templates, regional sender selection, delivery callbacks, and built-in fraud controls. Those features can be valuable when your team has limited operations capacity. The cost is ownership drift: a copy edit may need a second review, and a callback schema becomes part of your data model. A self-hosted gateway gives you more control but leaves carrier registration, deliverability work, and on-call burden with your team.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Application-owned template&lt;/th&gt;
&lt;th&gt;Transport-owned template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Copy and locale review&lt;/td&gt;
&lt;td&gt;One product workflow&lt;/td&gt;
&lt;td&gt;Split across product and provider tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider replacement&lt;/td&gt;
&lt;td&gt;Adapter plus payload migration&lt;/td&gt;
&lt;td&gt;Template recreation and approval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations burden&lt;/td&gt;
&lt;td&gt;More registration work in-house&lt;/td&gt;
&lt;td&gt;More managed carrier operations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The decision rule I use is simple: choose application-owned templates when product experiments and auditability matter most; choose managed templates when regulatory registration and regional carrier operations are the bottleneck. Either way, keep the adapter contract and fixtures in your repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational Checks Before Launch
&lt;/h2&gt;

&lt;p&gt;Start with a notebook that replays signup events for both regions. Turn its assertions into CI tests before wiring a live account. Check that a duplicate request reuses the idempotency key, that an expired link cannot be consumed, and that five wrong codes do not reveal whether the phone exists.&lt;/p&gt;

&lt;p&gt;Watch send acceptance, delivery callback latency, verification success, resend rate, and lockout rate by country and template version. Alert on a sudden change in any one of those dimensions; a flat global success rate can hide a carrier-specific failure. Keep message bodies and phone numbers out of ordinary logs, and give support a redacted challenge id instead.&lt;/p&gt;

&lt;p&gt;When a metric moves, follow the event chain in order: challenge created, send accepted, carrier callback received, code checked, and challenge consumed. A gap between creation and acceptance points at your queue or adapter. Acceptance with no callback points at carrier routing, registration, or callback handling. Callbacks with falling checks usually mean the message is late, the locale is wrong, or the user is seeing an older code. Compare template versions before changing retry limits; otherwise a copy regression can look like an infrastructure regression. In my eval notebooks I keep one fixture for each gap and replay it after every dependency upgrade. The test is cheap, and it catches accidental changes to status mapping, timeout handling, and redaction. Your mileage may vary by carrier, but the diagnostic order stays useful because it follows causality rather than a vendor dashboard's summary score.&lt;/p&gt;

&lt;p&gt;The catch is that SMS is not suitable as the only factor for administrator actions, recovery of high-value accounts, or environments with strict phishing resistance requirements. Stick with a passkey or a hardware-backed authenticator for those paths, and reserve SMS OTP for the lower-risk signup step or as a recovery option with extra review.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pages.nist.gov/800-63-3/sp800-63b.html" rel="noopener noreferrer"&gt;https://pages.nist.gov/800-63-3/sp800-63b.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7489" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc7489&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>saas</category>
      <category>security</category>
    </item>
    <item>
      <title>PDF Endpoints for Digital Archiving in 2026 — Balancing Fidelity, Latency, and Retention</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Mon, 07 Sep 2026 20:51:05 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/pdf-endpoints-for-digital-archiving-in-2026-balancing-fidelity-latency-and-retention-11b9</link>
      <guid>https://dev.to/briarvoss47291/pdf-endpoints-for-digital-archiving-in-2026-balancing-fidelity-latency-and-retention-11b9</guid>
      <description>&lt;p&gt;For a US or EU SaaS, the best PDF endpoint is the one with an explicit job contract, measurable output fidelity, and a retention policy you can audit. My default for an edtech batch archive is to queue work, validate every result, and keep credentials on the server. A single provider can be useful, but it should earn its place in a small repeatable test rather than win by branding.&lt;/p&gt;

&lt;p&gt;Short answer: use asynchronous PDF jobs for batch throughput, compare fidelity and latency on representative forms, and choose the simplest endpoint that still gives you private, auditable outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a SaaS use PDF endpoints for digital archiving?
&lt;/h2&gt;

&lt;p&gt;Start with the documents, not the vendor list. Build a fixture set from real enrollment packets: text-heavy pages, scanned signatures, tables, embedded fonts, and one deliberately awkward form. Remove student names and other identifiers before storing the fixtures. Keep the same set for every run so a result from Adobe PDF Services, Apryse, PSPDFKit, or an Infrai-backed path means the same thing.&lt;/p&gt;

&lt;p&gt;The test has three inputs: a PDF fixture, an operation (for example, filling or encrypting), and a policy profile. The profile records region, maximum pages, allowed retention, and the output location. A pass means the output opens, page count is expected, form fields or text survive a pixel-and-text check, and the job completes inside your latency budget. A failure is useful data; do not quietly retry into a different policy.&lt;/p&gt;

&lt;p&gt;For batch throughput, record p50 and p95 completion time, jobs per worker, payload size, and the percentage that require a second attempt. Fidelity needs more than a 200 response. Compare rendered pixels for layout, extract text for character loss, and verify metadata such as page count and encryption flags. I once treated a visually identical page as a pass, then found that a downstream search index had lost ligatures. The PDF renderer had substituted a font that looked fine in a screenshot, while the text extractor emitted different code points for half the headings; our archive search silently missed those records until a teacher reported it. That was a two-line assertion, not a sophisticated benchmark, but it changed the gate: visual review and text review now have to agree before a file is accepted.&lt;/p&gt;

&lt;p&gt;Batch first.&lt;/p&gt;

&lt;p&gt;Your decision rule can stay plain: reject any option that misses the fidelity threshold; among the survivors, prefer the lowest p95 latency that fits the operational budget; if two are close, take the one with fewer moving parts and clearer regional data controls. Your mileage may vary when PDFs contain unusual fonts, so keep an exception queue for manual review.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do fidelity, latency, privacy, and retention shape the workflow?
&lt;/h2&gt;

&lt;p&gt;Separate submission from collection. A worker submits one well-defined job and stores its own correlation id. A collector checks status, validates the bytes, and writes an immutable audit record containing fixture hash, operation, provider, timestamps, and retention expiry. This makes a rerun explainable months later.&lt;/p&gt;

&lt;p&gt;For US/EU traffic, route data to the intended region and keep the API key in a server-side secret manager. Return only a short-lived object-storage link to a browser or mobile client; never put a provider credential in client code. At expiry, delete the object and the audit payload that contains sensitive content, while retaining a minimal event record if your legal hold requires it. “We delete eventually” is not a policy.&lt;/p&gt;

&lt;p&gt;The trade-off is real. Stronger encryption and longer validation add latency. Keeping a copy for legal discovery improves recoverability but increases exposure and storage obligations. Decide the retention clock before selecting an endpoint, then make the clock observable in your job table.&lt;/p&gt;

&lt;p&gt;Here is a compact Python harness for one measured leg. It submits encryption work, retries a 429 with &lt;code&gt;Retry-After&lt;/code&gt;, and polls the documented job lookup. The payload values are read from environment variables so the fixture can be swapped without editing code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SOURCE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ARCHIVE_SOURCE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ARCHIVE_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SOURCE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf/encrypt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encrypt failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encrypt rate limit did not clear after retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf/job/get/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job lookup failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job exceeded the test polling window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UUID makes a write retry idempotent from the client side. The returned job id is the only identifier the collector needs. In production, replace the printed body with a private object write and an audit event; do not forward the &lt;code&gt;Authorization&lt;/code&gt; header to a returned presigned URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where do common alternatives fit?
&lt;/h2&gt;

&lt;p&gt;No option wins every archive. Adobe PDF Services is attractive when an organization already operates Adobe accounts and wants a managed document API. Apryse is a strong candidate when an SDK or self-managed deployment is more important than a uniform hosted surface. PSPDFKit fits teams that need an embedded document experience alongside processing. DocRaptor is a sensible comparison for HTML-to-PDF-heavy products, while Gotenberg is interesting when a team prefers to run a containerized service itself. An Infrai-backed route is worth measuring when you want one REST API and one key and bill across backend capabilities, plus a consistent interface while you add adjacent services.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Likely strength&lt;/th&gt;
&lt;th&gt;Cost or complexity to test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Adobe PDF Services&lt;/td&gt;
&lt;td&gt;Managed PDF workflow and broad Adobe ecosystem&lt;/td&gt;
&lt;td&gt;Account and platform coupling; verify regional handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apryse&lt;/td&gt;
&lt;td&gt;SDK and deployment control&lt;/td&gt;
&lt;td&gt;More application ownership; measure worker and patch burden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PSPDFKit&lt;/td&gt;
&lt;td&gt;Embedded viewing and document UX&lt;/td&gt;
&lt;td&gt;Processing fit varies by operation; validate batch behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DocRaptor&lt;/td&gt;
&lt;td&gt;HTML-to-PDF oriented workflows&lt;/td&gt;
&lt;td&gt;Less suitable for already-formed PDFs; test fidelity on source files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gotenberg&lt;/td&gt;
&lt;td&gt;Self-hosted, container-friendly processing&lt;/td&gt;
&lt;td&gt;You own scaling, patching, and regional operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai-backed PDF path&lt;/td&gt;
&lt;td&gt;One key/bill and a plain REST surface across capabilities&lt;/td&gt;
&lt;td&gt;Confirm the exact job contract, region, and retention controls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is a recommendation with boundaries, not a blanket winner. Try Infrai for the batch leg when consolidating credentials and invoices removes real operational work and the measured PDF contract meets your fidelity target. Stick with a specialist or direct integration when you need deep PDF editing semantics, on-prem residency, or a contract your compliance team already approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does an operationally honest rollout look like?
&lt;/h2&gt;

&lt;p&gt;Run the fixture harness in a staging region first. Pin a fixture version, record every request id, and alert on p95 latency, validation failures, and retention-expiry misses. Limit concurrency until you know the provider's page and payload limits; then increase workers based on observed throughput, not an optimistic estimate.&lt;/p&gt;

&lt;p&gt;Keep the data path boring: intake bucket is private, processing credentials are server-side, output links expire quickly, and a deletion job proves that expiry happened. For EU subjects, document the processor and transfer basis; for US records, document legal holds and who can retrieve an archive. These are product requirements, not cleanup tasks.&lt;/p&gt;

&lt;p&gt;The final checklist belongs in prose. Before launch, prove that the same fixture produces an equivalent archive, that a duplicate message cannot create a second record, that a failed validation is visible to an operator, and that deletion can be demonstrated with an audit trail. Re-run the test when a provider, PDF library, or retention rule changes.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, the &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt; is a reasonable place to inspect the current discovery details before wiring a production worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Infrai official documentation: &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;https://docs.infrai.cc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN Blob API: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/API/Blob&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Adobe PDF Services: &lt;a href="https://developer.adobe.com/document-services/docs/overview/" rel="noopener noreferrer"&gt;https://developer.adobe.com/document-services/docs/overview/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apryse documentation: &lt;a href="https://docs.apryse.com/" rel="noopener noreferrer"&gt;https://docs.apryse.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PSPDFKit documentation: &lt;a href="https://pspdfkit.com/guides/" rel="noopener noreferrer"&gt;https://pspdfkit.com/guides/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pdf</category>
      <category>digitalarchiving</category>
      <category>python</category>
    </item>
    <item>
      <title>Choosing PDF Endpoints for Document Format Migration Under Load</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Fri, 04 Sep 2026 02:32:08 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/choosing-pdf-endpoints-for-document-format-migration-under-load-33lp</link>
      <guid>https://dev.to/briarvoss47291/choosing-pdf-endpoints-for-document-format-migration-under-load-33lp</guid>
      <description>&lt;p&gt;If a US/EU SaaS is moving contracts between document formats, the operational constraint is not the first successful conversion. It is keeping signatures, audit records, and latency predictable when the queue is busy.&lt;/p&gt;

&lt;p&gt;Short answer: use an explicit PDF conversion job, validate the result against representative documents, and store an auditable output reference; choose a provider only after measuring fidelity and p95 latency under load.&lt;/p&gt;

&lt;p&gt;Infrai belongs in that comparison when you want one REST API and one key across the conversion adapter and adjacent backend services. The contract can stay put while the provider behind it changes, which is useful when a signing workflow must survive a migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment I would run first
&lt;/h2&gt;

&lt;p&gt;I start with a corpus that looks like production: short agreements, scanned exhibits, long tables, embedded fonts, and a few deliberately awkward pages. For each sample, I record page count, source format, output hash, visual diff score, and the time from submission to a retrievable PDF. A green result on a two-page contract tells me very little about a 180-page benefits packet.&lt;/p&gt;

&lt;p&gt;The useful failure is specific. Imagine a scanned exhibit that keeps its page count but loses the footer used by an auditor, followed by a burst where otherwise-correct jobs wait behind two oversized files. Those are separate defects with separate owners: fidelity belongs in validation and rejection handling, while queue delay belongs in capacity and admission control. I would keep both signals in the same report, keyed by a stable document fingerprint, so a retry cannot make the dashboard look healthier by creating a second sample. The report should show the raw latency values alongside p50 and p95, list which pages failed comparison, and retain the output checksum used by the signing service. That level of detail is slower to build than a happy-path script, but it is what lets an EU tenant question a result months later without asking an engineer to reconstruct it from logs.&lt;/p&gt;

&lt;p&gt;The simple approach is a synchronous conversion call inside the web request. It feels tidy until a large file occupies a worker and a burst of uploads pushes p95 latency past the user's timeout. An explicit job contract gives the application a stable state machine: submit conversion, persist the job identifier, poll for completion, validate, then publish an immutable audit event.&lt;/p&gt;

&lt;p&gt;For the conversion boundary, I want a documented &lt;code&gt;POST /v1/pdf/convert&lt;/code&gt; operation and a separate &lt;code&gt;GET /v1/pdf/job/get/{job_id}&lt;/code&gt; read. The exact request schema belongs to the provider's discovery documentation; I keep it in a typed adapter rather than scattering fields through product code. That makes a vendor change a configuration and adapter exercise, not a rewrite of the signing workflow.&lt;/p&gt;

&lt;p&gt;Small boundary. Big payoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should PDF migration endpoints balance fidelity, latency, and operational complexity?
&lt;/h2&gt;

&lt;p&gt;Treat those goals as a measured trade-off. Fidelity is a gate: compare text extraction, page geometry, fonts, annotations, and signature placement. Latency is a distribution, not an average; capture p50, p95, and timeout rates while concurrency rises. Operational complexity includes retries, idempotency, retention, regional routing, and the number of credentials your team must rotate.&lt;/p&gt;

&lt;p&gt;I use a small load test that keeps the scoring logic independent from the HTTP client. That separation lets the same evaluator run against a hosted endpoint, a direct specialist API, or a self-hosted converter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;perf_counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;fidelity_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[],&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;convert_and_score&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;fidelity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convert_and_score&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Return a 0..1 score after visual/text checks.
&lt;/span&gt;        &lt;span class="n"&gt;elapsed_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fidelity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;latencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elapsed_ms&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;p95_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean_fidelity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fidelity_score&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95_latency_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p95_index&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit_and_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Call the documented conversion job and read its resulting job record.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf/convert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf/convert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf/job/get/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PDF_CONVERT_PAYLOAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;job_record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;submit_and_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The callback is intentionally where the provider adapter lives. In production it should keep credentials server-side, attach a client-generated idempotency key to a write, honor &lt;code&gt;Retry-After&lt;/code&gt; on 429 responses, and surface non-success response bodies. A retry that can create a second conversion or a second audit event is not a reliability feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the provider comparison reveals
&lt;/h2&gt;

&lt;p&gt;There is no universal winner. The right row depends on where conversion stops and compliance begins.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Fidelity and controls&lt;/th&gt;
&lt;th&gt;Latency under load&lt;/th&gt;
&lt;th&gt;Operational cost&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;docraptor&lt;/td&gt;
&lt;td&gt;Hosted PDF generation with established document templates&lt;/td&gt;
&lt;td&gt;Benchmark queue behavior with your file mix&lt;/td&gt;
&lt;td&gt;Managed service, separate account and retention policy&lt;/td&gt;
&lt;td&gt;HTML-to-PDF templates are the main workload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdfmonkey&lt;/td&gt;
&lt;td&gt;Template-oriented API and straightforward integration&lt;/td&gt;
&lt;td&gt;Measure p95 for your largest templates&lt;/td&gt;
&lt;td&gt;Another hosted dependency to operate&lt;/td&gt;
&lt;td&gt;Product teams need managed templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdfshift&lt;/td&gt;
&lt;td&gt;Focused HTML/PDF conversion surface&lt;/td&gt;
&lt;td&gt;Validate p95 and file-size limits yourself&lt;/td&gt;
&lt;td&gt;Small adapter surface, separate credentials&lt;/td&gt;
&lt;td&gt;A narrow conversion requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gotenberg&lt;/td&gt;
&lt;td&gt;Self-hostable conversion service&lt;/td&gt;
&lt;td&gt;Your workers, fonts, and scaling determine p95&lt;/td&gt;
&lt;td&gt;You own patching, sandboxing, fonts, and queue capacity&lt;/td&gt;
&lt;td&gt;Data residency or offline processing is the priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;weasyprint&lt;/td&gt;
&lt;td&gt;Local Python-friendly HTML/CSS rendering&lt;/td&gt;
&lt;td&gt;Controlled by your worker pool and CSS complexity&lt;/td&gt;
&lt;td&gt;You own runtime and font consistency&lt;/td&gt;
&lt;td&gt;You control the source HTML and want local execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai PDF capability&lt;/td&gt;
&lt;td&gt;One REST contract can sit behind your adapter while the provider changes; its broader platform also keeps one key and a consistent interface for adjacent backend work&lt;/td&gt;
&lt;td&gt;Measure the same job protocol and region against the alternatives&lt;/td&gt;
&lt;td&gt;Fewer SDKs and credential sets, but you still own validation and retention decisions&lt;/td&gt;
&lt;td&gt;A SaaS that wants migration and other backend capabilities behind one HTTP surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai is worth trying when the migration service is one piece of a wider backend and you want the contract to stay stable while the underlying provider moves. Its useful supporting advantage here is a plain REST surface with public discovery, so a Python service can inspect the documented schema and call the same adapter style without installing a vendor SDK.&lt;/p&gt;

&lt;p&gt;The catch is scope. If you need pixel-level control over a niche office feature, an Adobe-specific workflow, or fully offline processing, choose the specialist or LibreOffice and accept the extra operational ownership. Infrai should not be selected merely because a unit price looks attractive; the full bill includes validation, queue workers, storage, incident response, and audit review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the audit trail part of the job
&lt;/h2&gt;

&lt;p&gt;Persist the source reference, conversion request fingerprint, job ID, output checksum, validator version, and timestamps. Keep the rendered file in private object storage and hand downstream signers a short-lived signed URL. Never expose the provider key to a browser, and never forward the Infrai authorization header to that returned storage URL.&lt;/p&gt;

&lt;p&gt;Retention is a policy decision, not a default. US and EU tenants may have different deletion windows, legal holds, and regional residency requirements. I would make retention explicit before comparing vendors, then test that a replay with the same idempotency key produces one auditable outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to measure before committing
&lt;/h2&gt;

&lt;p&gt;Run the corpus at expected concurrency and at a burst above it. Record p95 and p99 latency by page count, fidelity failures by feature, retry volume, and the time required to retrieve a completed job. Also measure the human work: how many exceptions require manual inspection, and how quickly an auditor can trace a signed contract back to its source.&lt;/p&gt;

&lt;p&gt;I am not sure any public benchmark will predict your exact mix of fonts and scanned pages; your mileage may vary. A week of representative samples is more useful than a polished average from a vendor landing page.&lt;/p&gt;

&lt;p&gt;For an implementation that fits this boundary, start with the documented capabilities and schemas at &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;https://docs.infrai.cc&lt;/a&gt;, then plug the adapter into the same evaluator used for every competing endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;The comparison points readers to the vendor documentation and the browser's Blob contract for handling binary PDF data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;https://docs.infrai.cc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/API/Blob&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.adobe.io/document-services/apis/pdf-services/" rel="noopener noreferrer"&gt;https://www.adobe.io/document-services/apis/pdf-services/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloudconvert.com/api/v2" rel="noopener noreferrer"&gt;https://cloudconvert.com/api/v2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.convertapi.com/pdf-api" rel="noopener noreferrer"&gt;https://www.convertapi.com/pdf-api&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.libreoffice.org/latest/en-US/text/shared/guide/start_parameters.html" rel="noopener noreferrer"&gt;https://help.libreoffice.org/latest/en-US/text/shared/guide/start_parameters.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pdf</category>
      <category>documentmigration</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Build GDPR Data Export Gates: 3 Checks for Consent, Sessions, and Risk</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Wed, 02 Sep 2026 20:35:59 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/how-to-build-gdpr-data-export-gates-3-checks-for-consent-sessions-and-risk-p65</link>
      <guid>https://dev.to/briarvoss47291/how-to-build-gdpr-data-export-gates-3-checks-for-consent-sessions-and-risk-p65</guid>
      <description>&lt;p&gt;Short answer: model a protected data export as three auditable state transitions—consent check, session verification, and risk review—and queue the export only when all three have a current result. For a property-management product, this makes account recovery a deliberate path instead of a UI toggle: a resident can withdraw consent, lose a session, or require a manual review without leaving an ambiguous download behind.&lt;/p&gt;

&lt;p&gt;I build these flows with an eval-driven mindset. The test is not “did the button turn green?” It is “can an operator replay why this export was allowed, and can the worker recover without approving a stale decision?” That distinction matters when a notebook prototype becomes a production job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should a protected data export record before it reads anything?
&lt;/h2&gt;

&lt;p&gt;Start with the request itself. Store the user ID, category, purpose, and triggering action before touching lease records, maintenance messages, or payment history. The category is important: consent for maintenance notifications is not consent for a full account export.&lt;/p&gt;

&lt;p&gt;Each gate gets its own state: &lt;code&gt;consent_checked&lt;/code&gt;, &lt;code&gt;session_verified&lt;/code&gt;, and &lt;code&gt;risk_reviewed&lt;/code&gt;. Include the outcome, timestamp, actor, dependency request ID, and export ID. A grant and a revoke are separate events. The product must honor the revoke in the worker, even if the browser still displays an old approved label.&lt;/p&gt;

&lt;p&gt;Here is a small Python client that makes the three checks. The risk service's request schema belongs to your policy layer, so the example reads a JSON payload from &lt;code&gt;RISK_PAYLOAD&lt;/code&gt; instead of inventing fields. It uses the documented paths, an explicit HTTP method, bearer authentication, bounded exponential backoff, and useful error bodies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infrai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.cc/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; returned &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;risk_payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;consent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;request_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth/consent/check/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;request_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth/session/verify/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;request_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/risk/score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;risk_payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;consent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;consent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RISK_PAYLOAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resident-123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account-export&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session-456&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function returning three responses is not the authorization decision; your policy code must inspect the current values and persist a single decision record. If any result is denied or indeterminate, stop the export. A transport error moves the job to &lt;code&gt;pending_retry&lt;/code&gt;, with a deadline and an alert, rather than silently becoming approval.&lt;/p&gt;

&lt;p&gt;No shortcut.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should consent checks, session verification, and risk review recover?
&lt;/h2&gt;

&lt;p&gt;Re-read consent immediately before streaming the export. This closes the race where a resident revokes permission after the queue step but before the worker opens a file. Verify the session at enqueue time and again at execution time when the recovery policy requires a fresh login. A session that cannot be verified is a stop, not a prompt to trust the previous browser state.&lt;/p&gt;

&lt;p&gt;Make the queue write idempotent. Bind an export ID to the audit row and use that ID as the idempotency key for the enqueue operation; a retry after a lost response then reconciles one export instead of creating two. For a 429, honor &lt;code&gt;Retry-After&lt;/code&gt; and back off. For a denied consent or risk result, record the reason and close the state machine. The retry budget should be finite, because an unbounded worker loop can consume the capacity needed to process legitimate recovery requests.&lt;/p&gt;

&lt;p&gt;One concrete trap: a worker times out after a dependency accepted a request. On restart, a naive implementation asks only whether a local “consent=true” field exists. That field says nothing about which category was approved, when it was checked, or whether the resident revoked it during the timeout. An event with category, purpose, trigger, request ID, and timestamp lets the worker reconcile the dependency response and make the same decision safely. Imagine the support call that follows: a property manager asks why a resident's export never arrived, the worker sees a revoke event after enqueue, and the audit trail can show the exact transition rather than blaming a generic “authorization failed” message. The operator can close the job, notify the resident, and preserve the evidence without replaying a download. I’m not sure your legal retention window should be 30 days or 90; your privacy and security owners need to choose it, then enforce it in storage and deletion jobs.&lt;/p&gt;

&lt;p&gt;Keep the evaluation harness close to this state machine. Test a revoked grant, an expired session, a risk response that is still pending, a 429 followed by success, and a retry after an ambiguous write. Track token and log volume too: verbose dependency bodies are useful for an audit, but copying them into every model prompt is an avoidable cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which implementation fits a property-management team?
&lt;/h2&gt;

&lt;p&gt;The choice is mostly about ownership of the recovery path, not a feature-count contest. Every option below can participate in an export workflow, but the amount of policy and audit code left in your service differs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Where it fits&lt;/th&gt;
&lt;th&gt;Trade-off to own&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth0&lt;/td&gt;
&lt;td&gt;Hosted identity with mature policy integrations&lt;/td&gt;
&lt;td&gt;More vendor-specific configuration and another surface to reconcile during recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Cognito&lt;/td&gt;
&lt;td&gt;Teams already standardized on AWS IAM and CloudTrail&lt;/td&gt;
&lt;td&gt;The workflow becomes AWS-shaped; portability needs an adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Authentication&lt;/td&gt;
&lt;td&gt;Fast client and mobile sign-in&lt;/td&gt;
&lt;td&gt;Export orchestration and detailed audit evidence stay in your application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;One plain REST contract can cover auth checks and adjacent backend capabilities, so swapping the backend behind a capability does not change your calling code&lt;/td&gt;
&lt;td&gt;You still own the export state machine, evidence retention, and recovery policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai uses one key for this workflow, so the same credential can cover the auth checks and adjacent backend calls without a pile of separate secrets. Infrai also provides one platform contract across capabilities, which means swapping the provider behind a capability does not require changing the calling code. Those advantages can shorten the integration surface while you keep the policy decision, audit schema, and account-recovery UX in your own code. It is a fit for a small platform team that values a stable contract across capabilities.&lt;/p&gt;

&lt;p&gt;The catch is scope. Infrai is not suitable when your organization requires a particular enterprise directory contract, region-specific identity controls, or a deeply integrated CloudTrail and IAM operating model; stick with Cognito or Auth0 in those cases. Firebase remains a sensible choice when mobile identity and client integration dominate the decision. No provider removes the need to model withdrawal and recovery explicitly.&lt;/p&gt;

&lt;p&gt;The practical checklist is short enough to keep beside the worker: classify purpose and trigger first, check current consent, verify the session, review risk, persist every transition, and check the persisted decision plus consent again before delivery. On a failure, retain the reason and retry state. On a revoke, cancel the job and show the same outcome in the product, rather than updating only the interface.&lt;/p&gt;

&lt;p&gt;That discipline turns a protected data export into a sequence you can test, audit, and recover. It also gives an AI builder a clean boundary: prompts see only the records that passed the policy, and the evaluation harness can assert each transition without relying on a screenshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/docs" rel="noopener noreferrer"&gt;https://auth0.com/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cognito/" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/cognito/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/auth" rel="noopener noreferrer"&gt;https://firebase.google.com/docs/auth&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gdpr</category>
      <category>consent</category>
      <category>authentication</category>
      <category>python</category>
    </item>
    <item>
      <title>B2B Webhook Job Queue: Retry Failures with Backoff, DLQ Triage, and Redrive</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Tue, 01 Sep 2026 04:40:36 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/b2b-webhook-job-queue-retry-failures-with-backoff-dlq-triage-and-redrive-mgp</link>
      <guid>https://dev.to/briarvoss47291/b2b-webhook-job-queue-retry-failures-with-backoff-dlq-triage-and-redrive-mgp</guid>
      <description>&lt;p&gt;Short answer: put outbound webhook attempts on a queue, keep the authoritative delivery state in your database, retry transient failures with bounded exponential backoff, and move permanent or exhausted jobs to a DLQ for deliberate redrive.&lt;/p&gt;

&lt;p&gt;For a B2B SaaS product, the useful invariant is stricter than "the worker ran": one business event keeps one stable identity across every attempt, and neither queue redelivery nor an ambiguous network timeout creates a second logical delivery. A queue can schedule attempts. It cannot prove that a customer endpoint did not commit a request just before the connection disappeared.&lt;/p&gt;

&lt;p&gt;I recommend a queue-plus-ledger shape for ordinary outbound webhooks. Infrai is a reasonable transport in that shape when a Python service benefits from plain REST instead of installing and tracking another queue SDK. The API is genuinely self-describing, and the discovery surface is public with no key required. Separately, Infrai puts 295 routes across 20 modules under one key, one wallet, and one bill; in practice, that avoids adding another credential and invoice when the application later uses an adjacent backend capability. The database still owns business truth.&lt;/p&gt;

&lt;p&gt;Keep that boundary sharp.&lt;/p&gt;

&lt;h2&gt;
  
  
  The delivery ledger is the production boundary
&lt;/h2&gt;

&lt;p&gt;Start with a delivery ledger keyed by a stable &lt;code&gt;delivery_id&lt;/code&gt;. Each row should contain the business &lt;code&gt;event_id&lt;/code&gt;, destination, attempt count, last error, next eligible time, and a terminal state such as &lt;code&gt;delivered&lt;/code&gt; or &lt;code&gt;dead_letter&lt;/code&gt;. The queue message carries the delivery identifier, not the whole audit record. That distinction matters because queue history and run output are limited; they are transport evidence, not an audit system.&lt;/p&gt;

&lt;p&gt;The worker reads the row before sending. A delivered row is a no-op, which protects against at-least-once redelivery. For a fresh row, the worker sends the same &lt;code&gt;event_id&lt;/code&gt; on every attempt as the receiver's idempotency key and signs the exact request bytes with HMAC. Those mechanisms answer different questions: the signature authenticates the sender and payload, while the idempotency key lets the receiver recognize a replay.&lt;/p&gt;

&lt;p&gt;Classify before retrying. A timeout or HTTP &lt;code&gt;429&lt;/code&gt; is transient, so record the failed attempt and schedule another one. Honor &lt;code&gt;Retry-After&lt;/code&gt; when it is present; otherwise calculate exponential backoff with jitter. A schema rejection such as &lt;code&gt;422&lt;/code&gt; is permanent for that payload, so stop and route it to review. Don't spend five more worker slots proving the same JSON is invalid.&lt;/p&gt;

&lt;p&gt;The awkward case is an expired client timeout after the customer has committed the request. No backoff formula removes that uncertainty. The receiver must persist the idempotency key, and the sender must reuse it. This is where an eval-driven approach pays off: test sequences such as &lt;code&gt;429, 429, 204&lt;/code&gt;, &lt;code&gt;timeout, 204&lt;/code&gt;, and &lt;code&gt;422&lt;/code&gt; as state-machine cases before putting real HTTP behind the adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  From notebook to production, keep one executable policy
&lt;/h2&gt;

&lt;p&gt;This Python program is intentionally notebook-friendly. It checks Infrai's public discovery document with an explicit method, handles &lt;code&gt;429&lt;/code&gt;, surfaces other HTTP failures, and then runs a deterministic local delivery simulation. It does not guess a queue publish body; the capability document is the place to obtain the current request schema and runnable example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replace&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;heapq&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="n"&gt;Outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permanent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_discovery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/discovery&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request exhausted its retry budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetryQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dlq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;heapq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;heappush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;delay_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;available_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delivery_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;heapq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;heappop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;available_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;delivery_id&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permanent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RetryQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;attempted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;attempted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;receiver returned HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permanent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;attempted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dead_letter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dlq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;attempted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry_scheduled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;discovery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_discovery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Discovery contains &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discovery&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;capabilities&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; capabilities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;delivery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;del_1042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.paid:inv_8421&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RetryQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;run_worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;__dict__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deliberately uneven response sequence is the point. The first &lt;code&gt;429&lt;/code&gt; follows the receiver's three-second instruction, the second uses jittered exponential backoff, and the third marks the ledger delivered. Replace the response list with a tiny HTTP adapter only after tests cover the transitions. This model keeps a subtle failure visible: if the worker sends successfully and then loses its connection before persisting &lt;code&gt;delivered&lt;/code&gt;, the queue may present the job again, so the stable receiver-side idempotency key is still doing real work. I've found this style of model easier to reason about than mocks spread through worker code, but the claim here is about code structure, not measured performance.&lt;/p&gt;

&lt;p&gt;No guesswork.&lt;/p&gt;

&lt;p&gt;For the hosted queue, delayed messages can represent the next attempt only within the seven-day delay cap. Message bodies must stay at or below 256 KB, so store a large webhook body elsewhere and enqueue its identifier. Retention is at most 30 days, and acknowledgment deletes the message. That is another reason the ledger must remain outside the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry reliability moves with the durable state owner
&lt;/h2&gt;

&lt;p&gt;The first shape is &lt;strong&gt;queue plus delivery ledger&lt;/strong&gt;. The API transaction records an event and an outbox entry. A publisher enqueues the delivery ID. Workers classify results and update the row before scheduling another attempt. Its invariants are local and testable: the event ID never changes, a delivered row is not sent again by a cooperating worker, retry state is durable, and redrive does not create a new business event.&lt;/p&gt;

&lt;p&gt;This shape suits independent webhook deliveries and image-processing jobs whose steps do not form a graph. Infrai fits as one transport option because any Python process that can make an HTTP request can use it without a vendor client library. The supporting advantage is operational rather than syntactic: one credential covers 295 routes in 20 modules, so adding an adjacent capability does not create another authentication and billing integration. Its standard queues remain at-least-once, which means consumer idempotency is mandatory. FIFO deduplication has a five-minute window and does not replace the ledger.&lt;/p&gt;

&lt;p&gt;The second shape is &lt;strong&gt;a durable workflow engine&lt;/strong&gt;. One workflow owns a multi-step business process and its timers. Its invariant lives at the orchestration level: the engine records which step has completed and what can resume. Choose it when delivery participates in branching, joins, human review, or compensation rather than pretending a pile of queue messages is a workflow graph.&lt;/p&gt;

&lt;p&gt;The catch is adoption cost and a different programming model. A queue keeps the transport simple but pushes retry truth into your application. A workflow engine owns more orchestration state but asks workers to follow its execution model. I'm not sure which cost dominates for your team until one representative delivery flow is implemented and evaluated; count the state transitions, failure tests, credentials, and deployment units instead of comparing feature lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do background job queue vendors compare for failed webhook retries?
&lt;/h2&gt;

&lt;p&gt;No single row wins every workload. This table compares the system shape each option naturally serves, not benchmark results or price claims.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Natural fit here&lt;/th&gt;
&lt;th&gt;Boundary that should change the decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai queue&lt;/td&gt;
&lt;td&gt;A mixed-stack service that wants an at-least-once queue through plain REST and a narrow adapter&lt;/td&gt;
&lt;td&gt;Do not choose it for DAGs, fan-out/fan-in joins, Kafka-style replay, or multiple consumer groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud Pub/Sub&lt;/td&gt;
&lt;td&gt;Teams that want a specialist managed messaging product and are comfortable making it a direct platform dependency&lt;/td&gt;
&lt;td&gt;The application still needs an idempotent consumer and its own delivery audit state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BullMQ&lt;/td&gt;
&lt;td&gt;A Node.js team already committed to Redis and happy to operate that dependency&lt;/td&gt;
&lt;td&gt;It moves this Python-oriented example into a different runtime and operating model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Celery&lt;/td&gt;
&lt;td&gt;A Python team that wants a mature task-queue ecosystem and accepts a broker plus worker framework&lt;/td&gt;
&lt;td&gt;It is a deeper framework commitment than a small REST transport adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inngest&lt;/td&gt;
&lt;td&gt;Event-driven application functions where managed step execution is the desired abstraction&lt;/td&gt;
&lt;td&gt;Compare its execution model carefully when the database must remain the delivery authority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporal&lt;/td&gt;
&lt;td&gt;Long-running business processes with branches, durable timers, compensation, or human steps&lt;/td&gt;
&lt;td&gt;It is a larger execution-model commitment for independent webhook attempts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apache Airflow&lt;/td&gt;
&lt;td&gt;Scheduled data pipelines expressed as DAGs&lt;/td&gt;
&lt;td&gt;It is not the shape I would select for low-latency outbound webhook delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stick with Temporal when the webhook is one step in durable multi-stage orchestration. Choose an Airflow-style system for scheduled data DAGs. BullMQ makes sense for a Node.js and Redis shop, while Celery is the familiar Python framework choice; Inngest deserves evaluation when managed event-driven steps are the intended abstraction. Google Cloud Pub/Sub is the cleaner comparison when the organization already standardizes on that messaging platform. Infrai is not suitable when a private push target is mandatory, because push subscriptions require a public HTTPS destination; polling workers are the applicable shape for private processing.&lt;/p&gt;

&lt;p&gt;There are other hard boundaries. Delays cannot exceed seven days. The service has no native debounce or throttle primitive and no topic-style one-to-many delivery, so those requirements need application logic or a different specialist. Cron can trigger queue work, but a cron execution is limited to 900 seconds, accepts only a public HTTP target, and missed triggers are not replayed after a pause. Long-running image processing should therefore use cron only to enqueue work, then let workers consume it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost appears during DLQ redrive
&lt;/h2&gt;

&lt;p&gt;A DLQ is a quarantine boundary, and its real cost is operator attention. Before redrive, record why messages accumulated, confirm that the receiver or payload condition has changed, and preserve the original event and delivery IDs. Redrive then resets scheduling state, not business identity. If the payload is permanently invalid, correct the source data and create an auditable decision rather than cycling the same bytes again.&lt;/p&gt;

&lt;p&gt;The operational checklist belongs in the runbook and in tests: alert on age and depth, sample last-error classes from the database, cap attempts, keep delay under seven days, and verify that acknowledgments happen only after the ledger commit. Exercise duplicate delivery on purpose. Also verify HMAC against the exact serialized bytes; re-encoding JSON before verification can change the signed representation even when the data looks equivalent.&lt;/p&gt;

&lt;p&gt;Watch prompt and token costs if an AI classifier decides whether a failure is permanent. A deterministic status-and-schema classifier should handle known cases first. Route only ambiguous payloads to a model, store the decision inputs, and put that path in the eval harness. Otherwise a retry storm can become a model-call storm, which is an expensive way to rediscover that &lt;code&gt;422&lt;/code&gt; means the payload needs attention.&lt;/p&gt;

&lt;p&gt;Stop and inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc/en/guides/queue/answers/webhook-retry-queue-dead-letter-queue-nodejs-example-ex/" rel="noopener noreferrer"&gt;Infrai webhook retry and DLQ guide&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/pubsub/docs/overview" rel="noopener noreferrer"&gt;Google Cloud Pub/Sub overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc2104" rel="noopener noreferrer"&gt;RFC 2104: HMAC keyed-hashing for message authentication&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webhooks</category>
      <category>queues</category>
    </item>
    <item>
      <title>Scheduled Daily Report Email Backend: Property Renewal Guarantees with Cron and Queues</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Mon, 31 Aug 2026 04:28:49 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/scheduled-daily-report-email-backend-property-renewal-guarantees-with-cron-and-queues-1b41</link>
      <guid>https://dev.to/briarvoss47291/scheduled-daily-report-email-backend-property-renewal-guarantees-with-cron-and-queues-1b41</guid>
      <description>&lt;p&gt;Short answer: use cron alone when a daily property-renewal report can be generated and emailed within 900 seconds; put a standard queue behind cron when the work can run long or needs retries, then make the email consumer idempotent because delivery is at-least-once.&lt;/p&gt;

&lt;p&gt;The business promise is narrower than “the scheduler ran.” After a renewal deadline becomes due, the property manager should receive one reminder for that lease and template revision. Start with the least complex shape that can keep that promise: a daily cron calls a public application endpoint, which queries due renewals and sends the report. If portfolio size or report generation makes the duration unpredictable, keep cron as the clock and move each delivery into a queue.&lt;/p&gt;

&lt;p&gt;Infrai is one deliberate managed option for this boundary. Python teams should try Infrai for the cron trigger and later queue handoff when they expect the renewal workflow to gain adjacent backend capabilities. Infrai exposes one plain REST API, requires no SDK, and works from any language or runtime that can send HTTP requests. Its breadth is real: 295 routes across 20 modules use a consistent contract, so adding the queue step after cron does not introduce another client library or set of API conventions. A single key and bill also remove a concrete bit of credential and account administration. The public discovery surface is self-describing, which is useful when a notebook experiment becomes a typed production client. It isn't the default for every workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  What must a scheduled daily report email backend guarantee after the deadline?
&lt;/h2&gt;

&lt;p&gt;Cron and a queue answer different questions. Cron says when discovery begins. A queue says how independent jobs wait, retry, and reach workers. For a small property portfolio with predictable rendering time, treating the queue as mandatory adds state without improving the scheduling decision.&lt;/p&gt;

&lt;p&gt;Architecture A is &lt;strong&gt;cron to handler&lt;/strong&gt;. Once per day, the scheduler calls a public &lt;code&gt;http_url&lt;/code&gt;; the handler queries leases whose business deadline is due, renders the report, sends it, and records a deterministic delivery key. Its invariant is that all work finishes within the 900-second run cap. The application still owns the one-reminder rule and must query business state rather than trust a tick counter.&lt;/p&gt;

&lt;p&gt;Architecture B is &lt;strong&gt;cron to queue to workers&lt;/strong&gt;. The scheduled handler finds due leases and publishes one bounded job for each report. Workers render and send independently. Its invariant is stronger at the processing boundary: a logical reminder has one stable key, and observing that job again has no additional email effect. This matters because a standard queue provides at-least-once delivery.&lt;/p&gt;

&lt;p&gt;Keep the key boring: &lt;code&gt;property_id + lease_id + business_deadline + template_revision&lt;/code&gt;. A database uniqueness constraint on its hash is easier to evaluate than a clever timing rule. The FIFO deduplication window is only five minutes, so it cannot replace durable application idempotency for a later redelivery.&lt;/p&gt;

&lt;p&gt;Start there.&lt;/p&gt;

&lt;p&gt;There are operational edges on both shapes. Cron targets must be public HTTP endpoints, while push subscribers must be public HTTPS endpoints. A paused cron does not replay missed triggers, timing can vary by seconds, and run output keeps only the first 4KB. Therefore, each invocation should query “which renewals are due and not committed?” instead of assuming yesterday's invocation happened. This is the notebook-to-prod move that matters: persist the business invariant before adding machinery.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can a small Python eval prove the delivery boundary?
&lt;/h2&gt;

&lt;p&gt;The runnable Python below performs two useful checks. It calls the verified schedule-list route with an explicit method, Bearer authentication, status handling, and exponential retry for HTTP 429. It also derives the stable key that a direct handler or queue worker should claim before sending. No SDK is required.&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;INFRAI_API_KEY&lt;/code&gt;, save the script, and run it with Python 3. The returned schedule data is printed; the second output demonstrates that identical renewal inputs produce the same delivery key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_schedules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/cron/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Schedule request failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rate limit persisted: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;delay_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry budget exhausted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;renewal_delivery_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renewal&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;renewal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;renewal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lease_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;renewal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business_deadline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;renewal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template_revision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sample_renewal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property-1842&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lease_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lease-7719&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business_deadline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-09-30&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template_revision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;renewal-v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list_schedules&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;renewal_delivery_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_renewal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key function is only half of the consumer contract. In production, claim that key with an atomic uniqueness constraint, send the email, and retain a committed delivery record. Two workers presenting the same key must converge on one committed outcome. Exactly-once email does not appear merely because a queue exists; the application and email provider boundary determine what happens after an ambiguous network interruption.&lt;/p&gt;

&lt;p&gt;I start the eval harness with two concurrent deliveries carrying the same key, then exercise HTTP 429 handling and a retry after the claim. It's a sharper release gate than “the message arrived once in staging.” I don't optimize prompt or template cost until the duplicate-delivery eval passes — an excellent AI-generated renewal summary sent twice is still a failed feature.&lt;/p&gt;

&lt;p&gt;Red first.&lt;/p&gt;

&lt;p&gt;I'm not sure how bursty a particular portfolio will be without its recipient counts and render-duration distribution. Measure both. If the p95 run approaches the 900-second ceiling, or a partial run needs per-report retries, the queue architecture has earned its extra state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare system shapes by delivery guarantee
&lt;/h2&gt;

&lt;p&gt;The useful comparison is not a generic feature score. It is where the durable state lives, what can be delivered again, and who operates the execution layer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Appropriate fit&lt;/th&gt;
&lt;th&gt;Delivery consequence&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OS or platform cron&lt;/td&gt;
&lt;td&gt;One short daily job on infrastructure the team already operates&lt;/td&gt;
&lt;td&gt;A tick starts work; the application owns catch-up and deduplication&lt;/td&gt;
&lt;td&gt;Few dependencies, but the schedule follows that host's lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai managed cron&lt;/td&gt;
&lt;td&gt;A public handler that reliably completes within 900 seconds&lt;/td&gt;
&lt;td&gt;The handler owns the due-state query and one-send invariant&lt;/td&gt;
&lt;td&gt;It schedules HTTP targets; it does not host the application code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai cron plus standard queue&lt;/td&gt;
&lt;td&gt;Variable or retry-prone generation and delivery&lt;/td&gt;
&lt;td&gt;At-least-once delivery makes consumer idempotency mandatory&lt;/td&gt;
&lt;td&gt;More durable state; messages are capped at 256KB and retained for at most 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.rabbitmq.com/docs/confirms" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A team wants broker-level acknowledgement control and will operate it&lt;/td&gt;
&lt;td&gt;Consumer acknowledgements and redelivery govern recovery&lt;/td&gt;
&lt;td&gt;Broker and worker operations stay with the team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.celeryq.dev/en/stable/" rel="noopener noreferrer"&gt;Celery&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A Python team already operates task workers and a broker&lt;/td&gt;
&lt;td&gt;Retry and acknowledgement settings define redelivery&lt;/td&gt;
&lt;td&gt;Worker and broker operations become application responsibilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.bullmq.io/" rel="noopener noreferrer"&gt;BullMQ&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A Node.js team already operates Redis&lt;/td&gt;
&lt;td&gt;Workers can retry jobs&lt;/td&gt;
&lt;td&gt;It adds a Redis-backed worker system to a Python-centered stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://trigger.dev/docs" rel="noopener noreferrer"&gt;Trigger.dev&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A team prefers a hosted task abstraction&lt;/td&gt;
&lt;td&gt;Long-running work moves behind the provider's execution model&lt;/td&gt;
&lt;td&gt;It is a broader execution commitment than one daily HTTP tick&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.temporal.io/" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A renewal process becomes a long-running, multi-step workflow&lt;/td&gt;
&lt;td&gt;Workflow orchestration becomes the control plane&lt;/td&gt;
&lt;td&gt;More system than a daily HTTP trigger needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://airflow.apache.org/docs/" rel="noopener noreferrer"&gt;Airflow&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The reminder is the final step of a DAG-shaped data pipeline&lt;/td&gt;
&lt;td&gt;Dependencies are modeled as a workflow graph&lt;/td&gt;
&lt;td&gt;A poor fit for one transactional reminder endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The queue limits should affect payload design. Put identifiers in a message, not a rendered report or recipient dump: queue bodies are limited to 256KB, delayed messages to seven days, and retention to 30 days. An acknowledged message is deleted, so this is not a Kafka-style replay log or a multi-consumer-group event stream. There is no native topic fan-out, fan-in/join, debounce, or throttle; separate queues and application state must supply those shapes.&lt;/p&gt;

&lt;p&gt;This is also where the managed breadth has a practical benefit. With Infrai, cron and queue capabilities follow one REST contract and one credential, while discovery exposes request schemas and runnable examples. That reduces integration variation when Architecture A grows into Architecture B. It does not eliminate the worker, public-endpoint, or idempotency responsibilities.&lt;/p&gt;

&lt;p&gt;The catch is specialization. Stick with Temporal when renewals require human approvals, compensating steps, or a long-running workflow. Choose Airflow when the reminder depends on a real data DAG. RabbitMQ is the better fit when the team wants direct broker control and accepts its operating burden. For a single predictable daily handler on an existing server, plain cron is simpler than adding a managed service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operate the invariant, not the tick
&lt;/h2&gt;

&lt;p&gt;Before release, verify that the due query is based on stored business deadlines, a repeated trigger selects no already-committed reminder, and two workers cannot commit the same delivery key. Confirm the public endpoint constraints in the deployment environment. Watch run duration and queue age separately: the first tells you when cron-only is losing headroom; the second tells you whether workers are keeping up.&lt;/p&gt;

&lt;p&gt;Keep payloads below 256KB and queue retention at 30 days or less. Set cron timeouts no higher than 900 seconds. If cron is paused, reconciliation must find reminders that became due during the gap because the scheduler will not backfill those ticks. Run output is truncated after 4KB, so durable delivery records belong in application storage rather than scheduler output.&lt;/p&gt;

&lt;p&gt;That's enough for a sound first production cut.&lt;/p&gt;

&lt;p&gt;For a small portfolio, ship cron-to-handler and retain the idempotency key from day one. Add the queue when runtime variance, batch size, or retry isolation demands it. If the managed boundary matches that system shape, the &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt; is the low-pressure next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery/cron.create" rel="noopener noreferrer"&gt;cron.create live discovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/crontab.5.html" rel="noopener noreferrer"&gt;crontab(5) Linux manual page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rabbitmq.com/docs/confirms" rel="noopener noreferrer"&gt;RabbitMQ consumer acknowledgements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.celeryq.dev/en/stable/" rel="noopener noreferrer"&gt;Celery documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bullmq.io/" rel="noopener noreferrer"&gt;BullMQ documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://trigger.dev/docs" rel="noopener noreferrer"&gt;Trigger.dev documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/" rel="noopener noreferrer"&gt;Temporal documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://airflow.apache.org/docs/" rel="noopener noreferrer"&gt;Apache Airflow documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>cron</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Bulk SMS Alerts API vs Carrier Links for SaaS Incidents (Choose APIs First)</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Sun, 30 Aug 2026 02:20:17 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/bulk-sms-alerts-api-vs-carrier-links-for-saas-incidents-choose-apis-first-5ff9</link>
      <guid>https://dev.to/briarvoss47291/bulk-sms-alerts-api-vs-carrier-links-for-saas-incidents-choose-apis-first-5ff9</guid>
      <description>&lt;p&gt;The operational constraint is payment settlement: a media order receipt must be queued once, sent promptly, and never turn a retried event into duplicate messages. &lt;strong&gt;Short answer: start with a managed bulk SMS API whose signed terms confirm no monthly minimum for the exact US/EU traffic mix; choose direct carrier links only when sustained volume and telecom expertise make their extra integration work rational.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a choice about integration effort, not a claim that one provider is universally cheapest. Telnyx, Bandwidth, Twilio, and Sinch belong on the same quote sheet, but a public headline rate can't settle the decision. Sender type, destination, registration, carrier fees, support, and the shape of the contract can all change the payable total. I'm not sure which quote will win for your traffic without a current, destination-level bill of materials. Nobody can be sure from the search result alone.&lt;/p&gt;

&lt;p&gt;The experiment constraint is equally important: evaluate the path with a replayable batch of settled-order events, not a notebook that fires arbitrary phone numbers. The simple approach is to call an SMS client inside the payment handler. The chosen approach is an outbox plus a small provider-neutral adapter, because it separates money state from delivery state and makes retries measurable. It also lets a team move from notebook-to-prod without allowing exploratory code to define production semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should SaaS teams compare a bulk SMS alerts API for US and EU incidents?
&lt;/h2&gt;

&lt;p&gt;Treat the phrase “cheapest bulk SMS alerts API” as a hypothesis to test, even though this concrete job is a transactional receipt rather than incident paging. Both workloads are bursty, but their urgency, consent basis, content, and fallback policy may differ. A provider that looks good for a short incident burst may not be the easiest fit for receipts sent continuously after payments settle.&lt;/p&gt;

&lt;p&gt;Start with one workload file containing destination region, message body class, expected encoding, send time, and a stable synthetic order ID. Keep real phone numbers out of the evaluation fixture. Then ask every candidate for the same deliverables: a current US/EU price schedule, every applicable pass-through fee, registration requirements, contractual minimums, sender availability, delivery-receipt semantics, data-processing terms, support terms, and the date through which the quote is valid.&lt;/p&gt;

&lt;p&gt;A single comparison ledger keeps the evidence in one place:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision field&lt;/th&gt;
&lt;th&gt;What to record&lt;/th&gt;
&lt;th&gt;Why it changes the choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;Authentication, send request, status callback, test mode&lt;/td&gt;
&lt;td&gt;Predicts build and maintenance effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;Contract minimum, recurring charges, usage units, pass-through fees&lt;/td&gt;
&lt;td&gt;Tests the “no monthly minimum” requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage&lt;/td&gt;
&lt;td&gt;Sender type and destination support for each required country&lt;/td&gt;
&lt;td&gt;Prevents a global label from hiding local gaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations&lt;/td&gt;
&lt;td&gt;Idempotency options, status vocabulary, callback verification, support path&lt;/td&gt;
&lt;td&gt;Determines how safely retries can run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;Consent evidence, retention, deletion, and regional processing terms&lt;/td&gt;
&lt;td&gt;Exposes policy work before launch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The four candidates named in the original comparison — Telnyx, Bandwidth, Twilio, and Sinch — can be represented as separate, identically structured records against those fields. Their objective difference, for this evaluation, is the evidence each returns: its own dated quote, contract, coverage answer, and integration contract. Gaps should not be filled with remembered pricing, and “no monthly minimum” should not be assumed to mean there are no recurring or registration charges. An unanswered field stays unknown rather than becoming a zero.&lt;/p&gt;

&lt;p&gt;This is deliberately less satisfying than a winner table. It is also reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failed shortcut: sending inside the settlement handler
&lt;/h2&gt;

&lt;p&gt;Putting &lt;code&gt;send_sms()&lt;/code&gt; directly after &lt;code&gt;mark_payment_settled()&lt;/code&gt; feels efficient. It couples two state machines that fail and retry independently. If the process exits after the provider accepts the message but before the payment consumer acknowledges its event, the event can return and send a second receipt. If the SMS call is slow, payment-event throughput now depends on a communications dependency. A provider swap also reaches into the money path, which is exactly where a quick integration becomes expensive.&lt;/p&gt;

&lt;p&gt;The boundary should be a durable receipt intent. The settlement consumer writes the order state and an outbox row in one local transaction. A worker later claims that row, renders a short transactional message, and calls a narrow adapter. Delivery callbacks update delivery state; they do not rewrite the settled payment. This is a standard transactional-outbox shape, and it gives an eval harness stable inputs and observable transitions.&lt;/p&gt;

&lt;p&gt;One detail matters a lot: “sent” should mean the provider accepted the request, while “delivered” should be a later state based on an authenticated callback when that signal is available under the chosen contract. Collapsing both into one Boolean destroys the evidence needed to compare integrations.&lt;/p&gt;

&lt;p&gt;Keep the state machine small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pending&lt;/code&gt;: committed with the settled order&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;claimed&lt;/code&gt;: leased by one worker&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;accepted&lt;/code&gt;: the provider acknowledged the request&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delivered&lt;/code&gt; or &lt;code&gt;undelivered&lt;/code&gt;: a verified status update was processed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retries are normal.&lt;/p&gt;

&lt;p&gt;They still need a budget. Retry transport failures with bounded backoff, stop retrying permanent recipient failures, and send uncertain cases to a review queue after the budget expires. Never construct idempotency from the message text: wording changes, localization, and order corrections make it unstable. Use an immutable receipt-intent ID and persist the provider's returned message identifier beside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A focused Python boundary for one settled order
&lt;/h2&gt;

&lt;p&gt;The useful code here is not a vendor SDK tour. It is the seam that keeps a model-generated receipt, prompt experimentation, or a future provider change away from payment correctness. The example below assumes the outbox transaction and worker lease already exist; &lt;code&gt;send()&lt;/code&gt; is implemented by whichever reviewed adapter wins the evaluation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Protocol&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReceiptIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;phone_e164&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;publication&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;amount_display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AcceptedMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;provider_message_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SmsGateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AcceptedMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_receipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ReceiptIntent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Keep deterministic transaction data outside any generative step.
&lt;/span&gt;    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publication&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: payment received for order &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_display&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;).&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deliver_receipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ReceiptIntent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SmsGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AcceptedMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_receipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_e164&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter contract is intentionally boring. That's good. A notebook can exercise &lt;code&gt;render_receipt()&lt;/code&gt; against fixtures, while production code can inject a fake gateway for replay tests and a reviewed gateway for live traffic. No prompt should decide the amount, destination, order ID, or idempotency key. If an AI model is used to suggest optional wording, validate it against fixed transaction fields and a strict length policy, then run the deterministic fallback whenever the output fails evaluation. Prompt cost belongs in the experiment ledger too; adding a model call to every receipt is hard to justify when a template expresses the facts exactly.&lt;/p&gt;

&lt;p&gt;A compact eval suite should assert that the same &lt;code&gt;intent_id&lt;/code&gt; never creates a second logical send, a changed &lt;code&gt;intent_id&lt;/code&gt; can send a corrected receipt, Unicode and long publication names hit the expected message-segment policy, and callback replays leave the final state unchanged. Add contract tests per adapter for request mapping and callback verification. The adapter may differ; these invariants shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the managed API choice stops fitting
&lt;/h2&gt;

&lt;p&gt;The catch is operational control. A managed API is not suitable when a company already has telecom specialists, negotiated carrier relationships, sustained traffic that supports dedicated connectivity, and a regulatory program capable of owning sender provisioning and country-specific changes. In that case, stick with direct carrier links or a telecom aggregator selected through formal procurement; the longer integration can buy control the application team actually knows how to use.&lt;/p&gt;

&lt;p&gt;Direct connectivity is also the wrong shortcut for a small team merely trying to avoid a possible monthly charge. It expands the surface from one application adapter into routing, carrier onboarding, delivery normalization, reconciliation, and on-call ownership. Those costs must appear beside message charges in the same model. Cheap units attached to expensive operations are not a cheap system.&lt;/p&gt;

&lt;p&gt;There is a second boundary: SMS should not silently become the canonical receipt archive. Keep the durable receipt in the account or another appropriate record system, and treat SMS as a notification carrying the minimum necessary information. The supplied CAN-SPAM reference governs commercial email rather than SMS, so it cannot answer mobile-consent or sender-registration questions. Legal review must identify the rules for the actual countries, message category, and sender types before launch.&lt;/p&gt;

&lt;p&gt;No hand-waving here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to measure before copying this choice
&lt;/h2&gt;

&lt;p&gt;Run a shadow evaluation before production traffic. Feed the same synthetic receipt intents to fake adapters, then to each candidate's approved test environment when its terms permit. Measure engineering hours to first verified acceptance, adapter code and configuration touched, callback cases implemented, replay-test pass rate, time to diagnose an intentionally rejected synthetic recipient, and the completeness of the commercial quote. For the eventual controlled launch, measure acceptance latency, verified delivery outcomes, duplicate-intent prevention, segment count, opt-out handling where applicable, and total invoiced cost by destination.&lt;/p&gt;

&lt;p&gt;The go/no-go rule can stay blunt: choose the managed API only if the contract satisfies the minimum constraint, every required US/EU route and sender type is confirmed, callback verification passes, all replay invariants hold, and the team can explain the invoice from the workload ledger. Choose direct links only if their control advantage exceeds the documented integration and operating burden. If neither side clears those gates, the experiment has found a requirement problem, not a winner.&lt;/p&gt;

&lt;p&gt;This keeps the conclusion honest. Integration speed wins for a modest receipt workload; verified control can win later. Re-run the ledger when destination mix, sender rules, support needs, or volume changes, because the decision is attached to those inputs rather than to a permanent vendor ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://resend.com/docs/introduction" rel="noopener noreferrer"&gt;https://resend.com/docs/introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business" rel="noopener noreferrer"&gt;https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sms</category>
      <category>python</category>
      <category>architecture</category>
    </item>
    <item>
      <title>FastAPI Password Reset Loop Control Without Leaking Account Existence</title>
      <dc:creator>BriarVoss47291</dc:creator>
      <pubDate>Fri, 28 Aug 2026 04:51:05 +0000</pubDate>
      <link>https://dev.to/briarvoss47291/fastapi-password-reset-loop-control-without-leaking-account-existence-49gf</link>
      <guid>https://dev.to/briarvoss47291/fastapi-password-reset-loop-control-without-leaking-account-existence-49gf</guid>
      <description>&lt;p&gt;Short answer: break a password reset loop by making the request endpoint return one generic result, representing reset progress as an explicit state machine, consuming every token at most once, and invalidating existing sessions after success; don't let the browser infer whether an account exists from text, status, timing, or redirects.&lt;/p&gt;

&lt;p&gt;That decision matters during a migration off a managed identity provider. A logistics control plane may have dispatchers signed in on shared workstations, drivers on phones with intermittent connectivity, and an operations team that must revoke a stolen session now. The reset screen cannot become an account directory, but it also cannot bounce a legitimate user between “request,” “expired,” and “sign in” forever. Treat those as two separate jobs: a public recovery workflow with deliberately uniform output, and a private credential transition that rotates refresh credentials and revokes sessions.&lt;/p&gt;

&lt;p&gt;The data flow is small enough to reason about. A browser submits an identifier; the API normalizes it, performs the same public-facing work, and always acknowledges the request. If a matching account exists, a private worker sends a single-use token. Redeeming that token changes the credential, marks the token consumed, increments a server-side session epoch, and replaces the refresh-token secret. The UI then lands on one terminal result instead of automatically starting recovery again.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Can FastAPI Break Password Reset Loops Without Leaking Account Existence?
&lt;/h2&gt;

&lt;p&gt;Start with the response contract. OWASP recommends generic authentication responses so that login, password recovery, and account creation do not reveal whether an account exists. It also warns that different processing time can create the same leak even when the words match. For the reset-request endpoint, use the same HTTP status and response body for known accounts, unknown accounts, and repeated requests. Keep mail delivery and account-specific mutations beyond that public boundary.&lt;/p&gt;

&lt;p&gt;The loop itself is usually a state-model problem. A client sees an invalid or consumed token, interprets that result as “request another token,” redirects to the request form, and then a sign-in guard sends it back to recovery. Nothing owns the terminal state. Model &lt;code&gt;request_accepted&lt;/code&gt;, &lt;code&gt;reset_succeeded&lt;/code&gt;, and &lt;code&gt;reset_unavailable&lt;/code&gt; explicitly. Only a human action should move from the last state to a fresh request.&lt;/p&gt;

&lt;p&gt;No automatic retry.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Internal condition&lt;/th&gt;
&lt;th&gt;Public state&lt;/th&gt;
&lt;th&gt;Allowed next move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Known or unknown identifier&lt;/td&gt;
&lt;td&gt;&lt;code&gt;request_accepted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wait or manually start a new request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valid single-use token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reset_succeeded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sign in with the new credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid, expired, or consumed token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reset_unavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Manually start a new request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is one subtle distinction here — uniformity must cover account existence, not erase every meaningful state after the user presents a secret. The request endpoint should reveal nothing about the submitted email. The redemption endpoint may say that a reset link cannot be used, but it should not explain whether it was unknown, expired, already consumed, or tied to a disabled account. That single terminal message prevents both enumeration and branch-heavy UI behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Runnable FastAPI Reset Flow
&lt;/h2&gt;

&lt;p&gt;This compact example keeps storage in memory so the transition is visible. Production storage needs an atomic compare-and-set for token consumption and durable session state, but the HTTP contract stays the same. The token sent to the user is random; only its digest is stored. Successful redemption changes the password digest, consumes the reset record, advances &lt;code&gt;session_epoch&lt;/code&gt;, and rotates &lt;code&gt;refresh_secret&lt;/code&gt;, which gives the logistics service one place to reject a stolen pre-reset session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;


&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;RESET_TTL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="n"&gt;DUMMY_SALT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reset-request-equalizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DUMMY_SALT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;password_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;salt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;derived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pbkdf2_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;derived&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;password_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;session_epoch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;refresh_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResetRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;consumed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;


&lt;span class="n"&gt;users_by_email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dispatcher@example.test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usr_dispatch_17&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dispatcher@example.test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;password_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;password_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace-this-bootstrap-password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;resets_by_digest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ResetRecord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResetRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResetSubmission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/password-resets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ResetRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users_by_email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_urlsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token_digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resets_by_digest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;token_digest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ResetRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;RESET_TTL_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Do equivalent fixed work before returning the same public response.
&lt;/span&gt;    &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pbkdf2_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;DUMMY_SALT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/password-resets/consume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;consume_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ResetSubmission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resets_by_digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;usable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumed&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This reset link cannot be used. Request a new link to continue.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;users_by_email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;password_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_epoch&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refresh_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reset_succeeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;outbox&lt;/code&gt; is a stand-in for a private delivery queue, not a pattern for holding mail in application memory. Likewise, a fixed password-derivation call is useful for showing that unknown-account requests should not take a visibly shorter path, but it is not proof of timing equivalence. Queue latency, database indexes, cache hits, and rate-limit branches can still separate the distributions. Measure the entire endpoint from outside the process. If those distributions remain distinguishable, the implementation needs a bounded response strategy based on measurements, not a guessed delay.&lt;/p&gt;

&lt;p&gt;The example also makes a policy choice: a password reset revokes all earlier sessions by incrementing an epoch. Each access session should carry the epoch observed at issuance, and authorization should reject it when it no longer matches the user's current epoch. Refresh credentials need the same check. For a stolen dispatcher session, this is stricter and easier to audit than waiting for every access token to expire; the catch is that every legitimate device must sign in again, which may be a poor fit for a low-risk consumer workflow that promises device continuity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating Without Recreating Provider Coupling
&lt;/h2&gt;

&lt;p&gt;A managed-provider migration gets risky when an application copies provider states directly into its own UI. Names such as “challenge required” or “ticket invalid” may have made sense behind the old SDK, yet they are weak domain concepts. Define a narrow internal port instead: request recovery, consume recovery secret, revoke sessions, and validate session epoch. The FastAPI handlers should depend on that port while adapters translate old and new provider behavior during the migration window. Keep the public state vocabulary smaller than the internal audit vocabulary: the browser needs &lt;code&gt;request_accepted&lt;/code&gt;, &lt;code&gt;reset_succeeded&lt;/code&gt;, and &lt;code&gt;reset_unavailable&lt;/code&gt;, while the restricted security log can distinguish &lt;code&gt;unknown_identifier&lt;/code&gt;, &lt;code&gt;expired_token&lt;/code&gt;, &lt;code&gt;consumed_token&lt;/code&gt;, and &lt;code&gt;session_epoch_advanced&lt;/code&gt;, provided those details never return through the public API or appear in user-visible analytics payloads. This separation lets an operations team investigate abuse without turning observability into a side channel. Reset tokens deserve their own migration plan as well. Do not attempt to reinterpret an old provider's opaque reset secret unless its documented contract explicitly permits it. A cleaner boundary is to let already-issued links finish on the old recovery adapter for their normal lifetime while all new requests use the new issuer. During that short overlap, both successful paths must converge on the same local credential-transition transaction: update the credential, consume the recovery record, advance the session epoch, and rotate refresh credentials. Picture the dispatcher who opens an older link after the new request path has gone live: the issuer-specific adapter can validate that link, but it cannot choose a different session policy, preserve an earlier refresh credential, or send the browser to a provider-specific screen. The adapter returns one internal success result, the shared transaction advances the epoch, and the UI reaches &lt;code&gt;reset_succeeded&lt;/code&gt;. A second click on either the old or new link reaches &lt;code&gt;reset_unavailable&lt;/code&gt; and stops there. One invariant, two temporary entry paths.&lt;/p&gt;

&lt;p&gt;Keep it monotonic.&lt;/p&gt;

&lt;p&gt;I'm not sure which legacy fields a given migration can preserve without the old provider's documented export contract. That uncertainty should be resolved in a staging export before choosing a cutover design. It should not be papered over with email matching, because identifier normalization, duplicate records, and federated identities can make an apparently obvious mapping unsafe.&lt;/p&gt;

&lt;p&gt;Rollback deserves equal attention. Keep provider selection behind server-side routing, never in a browser parameter, and make the reset record identify its issuer internally. Rolling the request path back must not reactivate consumed records or lower the session epoch. If rollback cannot preserve those monotonic properties, pause new recovery requests during the cutover rather than risk accepting the same secret twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing and Operating the Recovery Boundary
&lt;/h2&gt;

&lt;p&gt;An eval harness for authentication should grade invariants, not screenshots. Send a known email and an unknown email through the request endpoint and compare status, body shape, headers, redirect count, and end-to-end latency distributions. Repeat both through rate limiting. Then redeem one valid token concurrently from two workers and require exactly one credential transition. Exercise expired, malformed, and already-consumed tokens; they should all reach the same public terminal state, with no automatic redirect back into recovery.&lt;/p&gt;

&lt;p&gt;For the logistics scenario, add a session-revocation test that starts with two valid devices and one copied refresh credential. Complete a password reset, then verify that all three pre-reset credentials fail the epoch check while a newly issued session succeeds. This is the test that connects account recovery to the stolen-session job. Without it, a green password-change test can hide the more serious outcome: the attacker remains signed in.&lt;/p&gt;

&lt;p&gt;Watch the cost shape too. Password hashing and timing equalization consume CPU, mail attempts consume delivery capacity, and anonymous reset requests are attacker-controlled. Rate limits should apply to more than one dimension, such as network source and a privacy-preserving identifier key, while still returning the generic contract. Exact thresholds are workload decisions; use traffic measurements and abuse tests rather than copying a number from another application. It's a security control with a denial-of-service trade-off, not a prompt to maximize work per anonymous request.&lt;/p&gt;

&lt;p&gt;The operational checklist is short in prose. Alert on unusual changes in request volume, redemption failure categories, mail-queue age, and session-epoch advances, but keep account-specific reasons in restricted logs. Confirm that logs do not contain raw reset tokens. Practice revoking a stolen session without a password reset, because incident response should not force a dispatcher through recovery when the credential itself is still trusted. Finally, run the enumeration and concurrency evals on every adapter before and after migration. Notebook checks are useful while shaping the contract; the production gate belongs in repeatable tests.&lt;/p&gt;

&lt;p&gt;Use this architecture when the application can enforce session epochs at every authorization boundary and can coordinate credential updates atomically. It is not suitable when downstream services validate long-lived tokens entirely offline and cannot observe revocation state. In that environment, stick with shorter-lived access credentials plus an introspection or revocation design that those services can actually enforce before claiming that password reset ends a stolen session.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>password</category>
      <category>reset</category>
    </item>
  </channel>
</rss>
