At 4:40 on a Thursday, the owner of a three-person landscaping company finishes a general liability application on your site. She clicks “Get my quotes,” and a spinner appears. Two carriers respond in under four seconds. A third takes eleven. The fourth, a regional carrier with an aging rating service, is still thinking. The page cannot render until every request returns, so the spinner keeps turning. At 90 seconds she closes the tab and calls the broker her brother-in-law uses.
Your logs the next morning show no errors, just one slow upstream call. The system worked exactly as designed, and the design lost the sale.
Why quote-to-bind breaks under real carrier behavior
Quote-to-bind looks like one user action, but it is a long chain of steps: enrich the submission with business and property data, run your own rating or appetite checks, request quotes from several carriers, route edge cases to an underwriter, collect payment, issue the policy, and generate documents. Each step talks to a different system, and most of those systems belong to someone else.
Carrier APIs are where the variance lives. Some return a bindable quote synchronously in a second or two. Some accept the request and promise a webhook later, sometimes hours later if a human underwriter is involved. Some rate-limit aggressively or return a 200 with an error buried in the body.
When the orchestration is a synchronous chain of HTTP calls, the slowest dependency sets the pace for everything. Eventually the gateway cuts the request off, and the user sees a timeout even though three good quotes were sitting in memory.
The worse failure is partial completion. Payment succeeds, then the issuance call times out. Was the policy created? Your service does not know. The customer has been charged, has no policy number, and support hears about it when they call. Paid-but-unissued policies are a compliance problem as much as a user experience problem.
Why the usual workarounds fall short
Most teams reach for the same fixes, and each one helps a little without addressing the root cause.
- Raising timeouts. The request survives longer, but the customer still waits, and you tie up more connections under load.
- Parallel HTTP calls in the request handler. Total latency drops to the slowest carrier rather than the sum, but that carrier still blocks the response, and a process restart loses every in-flight quote.
- Retry loops in application code. Hand-written retries are inconsistent across carriers, rarely include backoff or circuit breaking, and can hammer a struggling carrier.
- Cron jobs that sweep for stuck records. A nightly job eventually finds paid policies without a policy number, but by then the customer has noticed.
- Polling tables for webhook results. This works for one carrier. Across a dozen, each with its own callback format, the polling logic becomes its own fragile subsystem.
The common thread is that all of these treat quote-to-bind as a request that should finish quickly, when it is really a long-running business process that should survive delays, restarts and partial failures.
The general approach: decouple, orchestrate durably, compensate
The fix is architectural, and these principles apply whatever tooling you choose.
Decouple carriers with asynchronous messaging. The quote page should not hold a connection open to every carrier. Instead, the front end submits the application, gets a submission ID immediately, and receives quotes as they arrive. To stream results, have each carrier activity publish its quote as it completes; when_all only gates the next stage. Carrier requests go out through a message broker using pub/sub. A slow carrier now delays only its own card on the page, not the whole result.
Model the process as a durable workflow. Each quote-to-bind run becomes a workflow instance whose progress is persisted after every step. If a worker crashes after two carrier quotes, the workflow resumes from that point instead of dropping the request.
Fan out and fan in with per-carrier deadlines. Request quotes from all eligible carriers in parallel, give each a deadline that matches its known behavior, and aggregate whatever arrives in time. A carrier that misses the window is marked “no response,” not treated as fatal.
Treat webhooks as external events. When a carrier promises a callback, the workflow should pause and wait for a named event, paired with a timer. The callback handler simply raises that event on the right workflow instance. No polling tables required.
Compensate when later steps fail. If issuance fails after payment, the workflow should run an explicit compensating action: void or refund the payment, notify the customer, and open a task for operations. This is the saga pattern, and it turns “unknown state” into a defined outcome.
Apply resiliency policies per carrier. Timeouts, retries, backoff and circuit breakers should be configuration attached to each carrier, not scattered try/except blocks.
Implementing it with Dapr
Dapr, the open-source distributed application runtime and a CNCF graduated project, maps closely to these principles. It runs as a sidecar next to each service; the building blocks that matter most here are pub/sub, workflows and resiliency policies.
The pub/sub building block works across many brokers, including Kafka, RabbitMQ, Redis and managed cloud services. Messages use the CloudEvents format, delivery is at-least-once, and dead-letter topics catch messages that keep failing. A malformed carrier response ends up somewhere you can inspect instead of blocking the queue.
One insurtech, for example, improved multi-step policy creation using asynchronous message-based communication on Dapr.
Dapr Workflow handles the orchestration. Workflows are written as code (Python, .NET, Java, Go and JavaScript SDKs are available), and their history is event-sourced into a state store. After a crash, the workflow replays and completed activities are not re-executed, so a carrier quote already received is not requested twice. Orchestrator code must be deterministic, so carrier calls, payments and database writes go into activities.
Here is a condensed Python sketch of the quote stage plus an underwriter referral. Each carrier activity enforces its own deadline and returns a status rather than raising, so when_all always completes with whatever each carrier produced.
import dapr.ext.workflow as wf
from datetime import timedelta
wfr = wf.WorkflowRuntime()
retry = wf.RetryPolicy(first_retry_interval=timedelta(seconds=2), max_number_of_attempts=5, backoff_coefficient=2, max_retry_interval=timedelta(minutes=1))
@wfr.workflow(name=”quote_to_bind”)
def quote_to_bind(ctx: wf.DaprWorkflowContext, submission: dict):
enriched = yield ctx.call_activity(enrich_submission, input=submission, retry_policy=retry)
quotes = yield wf.when_all([ctx.call_activity(request_carrier_quote, input={“carrier”: c, “risk”: enriched}) for c in enriched[“carriers”]])
if enriched[“needs_referral”]:
evt = ctx.wait_for_external_event(“underwriter_decision”)
timer = ctx.create_timer(timedelta(hours=48))
winner = yield wf.when_any([evt, timer])
if winner == timer:
return {“status”: “referral_expired”, “quotes”: quotes}
decision = evt.get_result()
# use decision to adjust or withdraw quotes before payment
# payment, issuance and compensation follow as further activities
@wfr.activity(name=”request_carrier_quote”)
def request_carrier_quote(ctx: wf.WorkflowActivityContext, data: dict): …
The same event-plus-timer combination handles carriers that reply by webhook: your callback endpoint finds the workflow instance from the carrier’s reference ID and raises the event. If the callback never arrives, the timer fires and the workflow moves on.
For payment and issuance, structure the workflow as a saga. Charge the card in one activity, attempt issuance in the next, and if issuance ultimately fails after its retries, call a refund or void activity before ending. Durable history means compensation runs even if the process restarted in between.
Per-carrier behavior belongs in a Dapr Resiliency policy (a YAML resource with kind: Resiliency). You can define timeouts, constant or exponential retries, and circuit breakers, then apply them to specific apps or components. A slow carrier gets a longer timeout and a circuit breaker; a fast one gets tight settings. This assumes each carrier is reached through its own Dapr component or app ID, for example one adapter service per carrier.
Every Dapr sidecar emits Prometheus metrics, and OpenTelemetry traces propagate W3C trace context across service invocation and pub/sub, so one submission can be followed through each carrier call.
You can self-host all of this on Kubernetes or VMs, or use a managed option. Azure Container Apps offers Dapr as a built-in integration, and Diagrid, started by Dapr’s original creators, is one provider whose Catalyst platform runs Dapr-based workflows in hosted, bring-your-own-cloud or self-hosted setups. Evaluate any option against your own carrier mix and failure scenarios before committing.
Where AI agents fit (optionally)
Underwriting preparation suits AI agents: summarizing loss runs or drafting a referral note for a human underwriter. But agent steps are slow and occasionally fail, the same behavior that already hurts quote-to-bind. Running those agents on a durable workflow platform means an interrupted LLM call or tool call resumes from the last completed step instead of restarting the entire referral. Keep agents in the preparation lane and leave the bind decision with a human or your rules engine.
A practical roadmap
- Map every step from submission to delivered documents, noting external calls.
- Profile each carrier: tail latency, sync or async, webhook format, rate limits.
- Change the quote page to return a submission ID immediately and stream quotes as they arrive.
- Move carrier requests onto pub/sub with dead-letter topics configured.
- Wrap quote-to-bind in a durable workflow with fan-out/fan-in and per-carrier deadlines.
- Replace webhook polling with external events paired with timers.
- Add compensation for issuance failures that happen after payment and test it deliberately.
- Move timeouts, retries and circuit breakers into per-carrier resiliency policies.
Before go-live, confirm:
- ☐ Killing a worker mid-quote does not lose the submission or duplicate carrier requests
- ☐ A carrier that never responds produces a “no response” result, not a stuck page
- ☐ A late webhook after the timer fires is logged and handled safely
- ☐ A forced issuance failure triggers a refund or void every time
- ☐ One trace ID follows a submission across every carrier call
Conclusion
The landscaping company owner did not leave over price. She left because one carrier’s slowness became your page’s slowness. Quote-to-bind is a long-running process built on systems you do not control, and it deserves an architecture that admits that: asynchronous messaging so carriers cannot block each other, durable workflows so progress survives failure, external events for callbacks, and compensation so a charged customer always ends up with either a policy or a refund. Start with the slowest carrier and the riskiest failure, and build out from there.