What's the difference between synchronous and asynchronous tax calculation?
Both patterns produce a tax amount on the order. They differ in when the calculation runs, what blocks on the result, and which infrastructure layer carries the load.
Six dimensions matter when an engineering team is designing the calculation layer for a $20M to $80M DTC brand.
| Dimension | Synchronous | Asynchronous |
|---|---|---|
| When it runs | Inside the checkout request, blocking the response to the customer. | After the order is persisted, triggered by an event (typically the orders/create webhook on Shopify or the equivalent on a custom stack). |
| Latency budget | 150-300ms at p95 (the tax slice of a 1.5-3 second end-to-end checkout target). [1] | Seconds to minutes, bounded by consumer lag rather than customer-facing wall-clock time. |
| Customer visibility | The result appears on the cart and checkout summary before the customer commits. | Invisible to the customer when used correctly. Visible as an invoice variance when misused. |
| Failure handling | Cached-rate fallback, fail-open with post-order recalculation, or fail-closed at the order layer. | Exponential backoff, dead-letter queue (DLQ) for repeated failures, manual reprocessing. |
| Infrastructure | Tax API as a direct dependency of the checkout request path. | |
| Instrumentation | Request rate, p95 latency, error rate at the API tier. | Queue depth, consumer lag, DLQ depth at the queue layer. |
The architectures are not mutually exclusive. At $30M the calculation layer is typically synchronous on the checkout path and asynchronous on a few post-order operations. At $80M most brands run a full dual-path pattern: synchronous for the customer-facing display, asynchronous for the ground-truth record that feeds reconciliation and filing. The engineering decision is not "sync or async" but "which flows belong on which path, and how do the two reconcile."
Which checkout flows actually require synchronous calculation
The brands that ran sync-everywhere discover at roughly $50M that the checkout latency budget is eaten by tax. The brands that moved to event-driven async for the non-display flows cut p95 checkout latency by 30-50% without changing the customer experience. The question that resolves the refactor is which flows actually need the synchronous result, and the answer is short.
Three checkout flows require synchronous tax calculation.
- Display calculation at the cart and checkout pages. The customer needs to see the exact tax line before completing the order. The order summary on the confirmation step shows subtotal, shipping, tax, and total. The tax value must resolve before that screen renders, which puts the calculation call on the synchronous request path with a 150-300ms p95 budget. [1]
- Payment authorization. The final amount sent to the payment processor at authorization must include tax. Authorizing $98.50 and then collecting $103.50 because tax was added post-auth is a payment-authorization mismatch, not a tax problem, and most processors treat it as a chargeback risk. The authorized amount has to be locked at the moment authorization fires.
- Order persistence. The tax record on the order must be set before the order moves to fulfillment. Downstream systems (inventory reservation, fulfillment routing, customer notifications) read from the order record, and a missing or pending tax field in that record introduces ambiguity into the order lifecycle. The tax amount that was displayed and authorized has to land on the persisted order before the order changes state.
Every other flow tolerates an asynchronous path. Post-order taxability adjustments, refund recalculations, retroactive corrections after a rate-table update, the ground-truth calculation that feeds the reconciliation log: each can run minutes or hours after the order without any customer-visible impact, provided the customer-displayed total never changes.
TaxCloud's calculation API is usable in both patterns: as the synchronous calculation surface inside the checkout extension on Shopify or Shopify Plus, and as the asynchronous endpoint the order-created webhook handler calls from the queue consumer. The same integration covers both paths, with no duplicate jurisdiction logic between the sync and async surfaces.
How an event-driven webhook-based async pipeline works
The webhook-triggered async pipeline is the canonical pattern for moving non-display calculation off the synchronous path. The architecture is documented in Shopify's webhook reference [4] and in the AWS and Apache Kafka queue patterns. [2][3] The shape is the same regardless of queue choice.
The order-created event fires. On Shopify and Shopify Plus, this is the orders/create webhook delivered to a brand-controlled endpoint immediately after order persistence.[4] On custom checkouts, this is an event emitted from the order service onto an internal event bus.
The webhook handler enqueues the calculation request. The handler validates the event payload, derives a stable idempotency key from the order ID and line items, and writes the calculation request to the queue (SQS, Kafka, or equivalent). The handler returns 200 to the webhook source within the Shopify-required acknowledgement window (5 seconds). [4] Any work beyond enqueueing happens off the webhook request path.
An async worker pulls from the queue. A separate consumer fleet, sized independently from the synchronous API tier, pulls events off the queue and processes them. The worker reads the event, calls the tax engine with the calculation payload, and receives the rate result.
The response writes back to the order record with the calculation log persisted. The worker writes the tax amount, jurisdiction stack, calculation timestamp, and rate-source identifier to the order. The calculation log is a separate record, anchored to the order ID, that survives the order lifecycle and is the document a state auditor requests on day one.
Failures route to the DLQ. After a configurable number of retries with exponential backoff (typically three to five attempts), the event lands in a dead-letter queue for manual review. [2] The DLQ is a first-class part of the architecture, not a fallback. Events land there because the upstream system needs to be alerted, not because the event should be dropped.
Idempotency is non-negotiable on this path. The worker may receive the same event more than once because of at-least-once delivery semantics on SQS and Kafka. [2][3] The idempotency key (derived from order ID and line items at calculation time, never from a timestamp) ensures a second call for the same order returns the stored result instead of producing a duplicate calculation log.
When async creates a customer-experience problem versus a reconciliation problem
The customer-experience-versus-reconciliation trade-off plays out predictably. Synchronous calculation gives the customer an exact total at checkout. Asynchronous post-order recalculation gives a ground-truth tax record that might shift the total. The rule that resolves the design choice is one sentence: never change the customer-displayed total asynchronously.
The rule has two consequences, depending on what the async recalculation finds.
- Async recalculation that confirms the same total: reconciliation problem only. The customer-facing experience is consistent. The customer was charged $103.50, the order record shows $103.50, the calculation log shows $103.50. The async path is doing its job: producing the ground-truth record that survives audit. Any delay in the async write is a backlog issue at the reconciliation layer, not a customer issue.
- Async recalculation that changes the total: customer-experience problem. The customer was charged $103.50, the async recalculation determines the correct rate produces $104.10, and the order now carries a mismatch between what the customer saw and what the ground-truth record shows. The mismatch surfaces as an invoice variance, a refund or supplemental charge, or a silent change that customer service has to explain weeks later. None of these are good outcomes.
The implication is that asynchronous calculation must be designed around the assumption that the customer-displayed total is authoritative for the customer-facing experience, even if the ground-truth record diverges. The async pass updates the calculation log for reconciliation and filing purposes; it does not change the order total once the order is placed. Where the divergence is material (rate updates, taxability corrections, jurisdiction-specific rules that resolved differently in the async pass), the resolution is a discrete adjustment posted to the order with a clear audit trail, not a silent overwrite of the displayed total.
This is one reason the calculation log is load-bearing for the async path. TaxCloud's reporting API exposes per-order calculation logs that the async ground-truth recalculation writes to, letting the reconciliation pass identify variance between the sync-display rate and the async-ground-truth rate without scanning the full order history.
The instrumentation pattern for the async path
Webhook-triggered async pipelines fail in a recognizable pattern. Queue depth spikes during peak hours, the consumer cannot keep up, and tax records lag the order records by hours or days. The instrumentation that catches this is queue-depth alerting at the SQS or Kafka layer, not at the application layer. An application-layer health check on the consumer service returns 200 the entire time the queue is backing up, because the consumer is healthy; it is simply behind.
Four instrumentation primitives belong on every async tax pipeline.
Queue depth with alert threshold tied to expected peak throughput. On SQS, the ApproximateNumberOfMessagesVisible metric, paired with the ApproximateAgeOfOldestMessage metric, is the primary signal. [2]
The alert threshold is set against the expected peak event rate plus a margin: if the consumer fleet processes 50 events per second steady-state and peak hits 500 events per second, queue depth should drain back to baseline within minutes, not hours. An age-of-oldest-message threshold above 5 minutes during peak is the call-to-investigate signal.
Consumer latency (time from event published to processing complete). This is the end-to-end metric the customer does not see but reconciliation does. On Kafka, this is consumer lag per partition [3], typically expressed in offset count and converted to wall-clock time via the publish-rate baseline. The target is bounded lag (the consumer keeps up at peak) rather than zero lag.
DLQ depth and arrival rate. Events landing in the DLQ are events that retried to exhaustion. A DLQ that fills during peak indicates a systematic failure (a tax-engine outage, a payload-format change, a credentials rotation) rather than transient noise. [2] Alerting on DLQ arrival rate catches systematic failures faster than alerting on absolute DLQ depth.
Idempotency key tracking to prevent duplicate processing on retry. The worker logs the idempotency key on every successful processing event. A duplicate key arriving from a retried delivery returns the stored result without re-executing the tax-engine call. Tracking the duplicate-key rate is a leading indicator of upstream delivery anomalies (queue redelivery storms, consumer-restart cycles) that would otherwise show up only as anomalous tax-engine call volume.
The most expensive sync-versus-async mistake we have seen is the brand that refactored to async-only for cost reasons, then discovered at audit that the checkout calculation logs lagged the order records by hours during peak. Reconstruction took the controller's monthly close from 5 days to 10 days and produced exception-resolution work that compounded for three months. The lesson is not that async is dangerous; it is that the async path requires queue-layer instrumentation and DLQ discipline from day one, not after the first audit.
The dual-path pattern at $80M and the load-test specification
The brand at $80M typically runs both paths simultaneously: synchronous for the checkout-display calculation, asynchronous for the ground-truth record that feeds reconciliation and filing. The two stay in sync via idempotent recalculation post-order, with the idempotency key tied to the order ID and the line items at calculation time.
The dual-path architecture buys three operational properties.
- Lower checkout latency. The synchronous call hits a hot cache for display, often resolving in 60-120ms at p50, while the async path carries the heavier work (full taxability resolution, multi-jurisdiction rate-table reads, audit-log persistence) off the customer-facing wall clock.
- Decoupled tax-engine availability. A tax-engine outage on the async path produces a queue backlog, not a checkout outage. The synchronous path uses cached fallback when the API breaches its budget, and the async path catches up when the engine recovers.
- Reconciliation as a first-class output. The ground-truth log produced by the async path is the document filing and audit are built on. Producing it as a separate path, rather than as a side effect of the synchronous call, surfaces calculation lag and rate-source variance as observable metrics rather than reconciliation anomalies discovered at month-end.
The architecture is more complex than sync-only or async-only. The operational benefits compound at scale, and the implementation surface (queue, consumer fleet, DLQ, idempotency cache, reconciliation pass) becomes routine infrastructure rather than a custom integration.
The load-test specification follows from the dual-path shape. The synchronous path is tested as a request-rate problem: the tax API must hold p95 latency at expected peak request rate. The async path needs separate load testing because peak-event throughput hits the queue layer, not the synchronous API. The target metric is queue depth under load, not request rate. A test that produces 500 events per second on the queue and measures whether queue depth drains back to baseline within the configured window is the load test that surfaces consumer undersizing before Black Friday.
At a multi-state, multi-channel steady state, the question is no longer whether to split the calculation layer. It is what the operating model looks like once you do. Platforms like TaxCloud are designed for this pattern: a single calculation API usable in both the synchronous checkout-display path and the asynchronous order-webhook path, with native Shopify and Shopify Plus integration where the webhook pipeline is wired, a reporting API that exposes the per-order calculation log, and a persisted calculation trail that supports audit defense.