Suppose checkout needs to reserve stock.
CHECKOUT
│
│ ReserveStock
▼
INVENTORY
If inventory responds successfully, checkout can continue.
If inventory explicitly rejects the request because stock is unavailable, checkout can return a truthful business result.
But what happens when inventory says nothing?
CHECKOUT
│
│ ReserveStock
▼
INVENTORY
│
?
Perhaps inventory is slow.
Perhaps the network dropped the request.
Perhaps inventory committed the reservation but its response was lost.
Perhaps the process is no longer running.
Checkout cannot wait forever.
It needs a time boundary.
Then, once that boundary is reached, it needs to decide whether another attempt is useful and safe.
That introduces three related ideas:
- timeout
- deadline
- retry
I had been treating them as variations of one setting:
Wait for a while and try again if nothing happens.
That is too vague for a distributed application.
Each mechanism answers a different question.
| Mechanism | Question |
|---|---|
| Timeout | How long may this particular wait continue? |
| Deadline | After what point is the complete result no longer useful? |
| Retry | Should another attempt be made after an unsuccessful one? |
A timeout limits waiting. A deadline bounds the whole operation. A retry spends part of that finite budget on another attempt.
The design becomes safer when those responsibilities remain distinct.
1. One operation has one finite time budget
Waiting needs a boundary
A local function usually returns, panics or ends with the process.
A remote call can stop producing observable progress.
request sent
│
▼
no response
│
▼
caller continues waiting
The caller cannot instantly distinguish among:
- a slow server;
- a lost request;
- a lost response;
- a broken network path;
- a failed destination;
- work still waiting in a queue.
Without a timeout or deadline, the caller may retain finite resources indefinitely:
- a connection;
- a goroutine or thread;
- memory;
- a request slot;
- transaction context;
- a user session.
Enough permanently waiting calls can exhaust the caller even when the underlying problem lies elsewhere.
A time limit therefore protects more than user patience.
It protects the application’s capacity to continue serving useful work.
A timeout is a duration
A timeout usually expresses a maximum duration.
timeout:
500 milliseconds
Conceptually:
call begins
│
▼
wait up to 500 ms
│
├── result arrives
│ └── continue
│
└── time expires
└── stop waiting
A Go client might write:
ctx, cancel := context.WithTimeout(
parent,
500*time.Millisecond,
)
defer cancel()
response, err := inventoryClient.ReserveStock(
ctx,
request,
)
The timeout says:
This call may consume no more than 500 milliseconds from the point at which I begin it.
That is a useful local instruction.
It is not necessarily the complete business budget.
A deadline is the outer completion boundary
A deadline expresses the latest acceptable completion time.
deadline:
14:30:06.000
The caller is saying:
A result received after this point is no longer useful enough for me to continue waiting.
A timeout can be converted into a deadline:
deadline = current time + timeout
Suppose checkout begins at 14:30:05.000 with a total budget of one second.
Its deadline is 14:30:06.000.
That difference becomes important when the request passes through several services.
A retry is another attempt, not extra time
Suppose the first request reaches its local limit and the caller retries.
LOGICAL OPERATION
ReserveStock for order 5001
│
├── attempt 1
└── attempt 2
Attempt two is a new network interaction.
The server may observe both.
The retry does not extend the value of the result. It spends whatever useful time remains.
A retry is not extra time. It is another way to spend the time that remains.
2. The budget follows the work
One deadline should follow the operation
Consider this call chain:
STOREFRONT
│
▼
CHECKOUT
│
▼
INVENTORY
│
▼
DATABASE
The storefront allows the complete operation one second.
STOREFRONT
1,000 ms remaining
│
▼
CHECKOUT
850 ms remaining
│
▼
INVENTORY
650 ms remaining
│
▼
DATABASE
less again
The budget shrinks as work consumes time.
If every service instead starts a fresh one-second timeout, the path can continue long after the original caller has stopped waiting.
storefront waits 1 second
checkout starts another 1-second wait
inventory starts another 1-second wait
A user operation intended to finish within one second can continue consuming downstream capacity for several seconds.
Deadline propagation prevents each layer from quietly resetting the clock.
Propagate remaining duration, not trust in wall clocks
It is useful to teach deadlines as absolute points in time, but machines in a distributed system do not necessarily have perfectly synchronised clocks.
Conceptually, one deadline follows the work.
Across process boundaries, the safe value to propagate is the remaining duration, after subtracting time already spent, rather than blind trust in another machine’s wall-clock timestamp.
gRPC deadline propagation follows this model. It converts the incoming deadline to a timeout with elapsed time deducted before forwarding it downstream.
incoming deadline
│
▼
calculate remaining duration
│
▼
propagate reduced timeout
The important property is not that every clock agrees.
It is that no participant receives more time than the operation still owns.
Time already spent cannot be reissued
Suppose checkout has a two-second deadline.
It spends:
- 300 ms validating the request;
- 400 ms reading the basket;
- 200 ms calling catalog.
It has already consumed 900 ms.
Only 1,100 ms remain.
Checkout should not give payment a fresh two-second timeout merely because payment normally permits two seconds.
The payment budget must fit within:
remaining checkout time
minus
time reserved for later checkout work
Perhaps checkout still needs:
- 100 ms to commit the order;
- 100 ms to build and transmit the response;
- a safety allowance for variation.
A possible allocation is:
payment:
up to 800 ms
order commit and response:
reserved remainder
This turns the deadline into a budget rather than a decorative timestamp.
A dependency may receive a smaller local limit
The outer deadline is authoritative, but a dependency may deserve a smaller sub-deadline.
A cache lookup, for example, might receive only a few milliseconds because the caller can fall back to the source.
OUTER DEADLINE
│
├── cache sub-deadline
├── database sub-deadline
└── response reserve
The effective limit is:
minimum of
configured dependency limit
and
remaining outer budget
In Go, a child context preserves the earlier parent expiry:
dependencyTimeout := 200 * time.Millisecond
ctx, cancel := context.WithTimeout(
parentCtx,
dependencyTimeout,
)
defer cancel()
If parentCtx expires sooner, the child expires with it.
The deadline belongs to the caller’s purpose
There is no universal correct timeout for GetProduct.
The useful duration depends on who is calling and why.
| Caller | Possible budget |
|---|---|
| Interactive storefront | Hundreds of milliseconds |
| Internal batch process | Several seconds |
| Background reconciliation | Longer, but still bounded |
| Administrative operation | Operation-specific and operator-visible |
The service cannot infer one perfect deadline for every caller.
The caller understands when the result stops being valuable in its own workflow.
The service should still publish latency expectations and operational limits so callers can choose sensible budgets.
3. Choosing useful time boundaries
A timeout is not a performance target
Suppose an operation has a two-second timeout.
That does not mean two seconds is acceptable normal latency.
normal latency:
100 ms
service objective:
perhaps 250 ms
failure boundary:
2 s
If every successful request finishes at 1.9 seconds, the system may have no timeout errors while still providing dreadful service.
Latency objectives describe expected performance.
Timeouts prevent unbounded waiting.
They serve different purposes.
One request contains several kinds of waiting
An outbound call may contain:
- name resolution;
- connection establishment;
- TLS negotiation;
- request transmission;
- server queueing;
- server processing;
- response transmission.
A client setting described as request timeout may or may not cover all of them.
| Limit | What it may bound |
|---|---|
| Connect timeout | Connection establishment |
| Read timeout | One period waiting for network data |
| RPC deadline | The complete RPC |
| Workflow deadline | The complete business operation |
A system can contain all four.
They should compose into one coherent policy rather than contradict one another.
A read timeout may not bound the complete response
A read timeout can restart after every successful read.
A peer that sends a small amount of data regularly may therefore keep the interaction alive far longer than the intended complete operation limit.
read some data
│
└── read timer resets
read some data
│
└── read timer resets
continue for much longer
This slow-trickle behaviour is one reason a complete RPC or workflow deadline remains necessary.
A per-read limit protects stalled reads.
It does not necessarily bound the total interaction.
Connection setup changes the path
Suppose a service normally reuses established connections.
Most calls take 20 ms.
After a deployment, the first call on each client must perform:
- DNS resolution;
- TCP connection;
- TLS negotiation.
That first call takes 120 ms.
A 100 ms limit works in steady state and fails whenever connections are cold.
The problem may appear during:
- deployments;
- restarts;
- connection rotation;
- certificate changes;
- load-balancer replacement.
Possible responses include:
- including setup in timeout selection;
- establishing connections before accepting traffic;
- warming pools gradually;
- measuring cold and warm latency separately.
The time boundary must cover the path that actually occurs, not only the warm path used during comfortable measurements.
Choosing a timeout means choosing some false failures
Suppose a dependency completes 99.9% of calls within 300 ms.
A 300 ms timeout will abandon part of the slower tail, assuming the measurement represents the caller’s real path.
Some of those calls might have completed at 305 ms, 340 ms or 500 ms.
The timeout creates a chosen rate of false failure:
dependency would eventually respond
but
caller stops waiting first
A shorter timeout:
- protects caller resources sooner;
- leaves more budget for fallback or retry;
- abandons more slow successes.
A longer timeout:
- allows more calls to finish;
- leaves fewer retry or recovery options;
- retains resources longer during genuine failure.
The decision should consider:
- the value of a late result;
- the cost of waiting;
- the latency distribution;
- remaining workflow work;
- possible retry opportunities;
- the caller’s total budget.
It should not be copied from another service because the number looked decisive.
Tail latency matters
An average can conceal the requests most likely to reach the deadline.
most calls:
50 ms
some calls:
200 ms
rare calls:
2 s
The caller experiences the tail.
If one user request waits for several dependencies, the probability that at least one is slow increases.
CHECKOUT
│
├── catalog
├── inventory
├── payment
└── shipping
Timeout design should consider percentiles such as p50, p95, p99 and p99.9, together with the maximum useful end-to-end latency.
The rare slow calls are where deadlines, retries and resource accumulation meet.
4. What happens when time expires
A timeout means the caller stopped waiting
When a deadline expires, the caller knows:
I did not receive a successful result before my time budget ended.
It does not necessarily know:
- the server never received the request;
- the server stopped processing;
- the transaction rolled back;
- no business effect occurred.
CLIENT SERVER
ReserveStock
─────────────────────────►
reserve item
commit transaction
deadline expires
DEADLINE_EXCEEDED
send response too late
A timeout is a conclusion about the caller’s waiting.
It is not a remote rollback instruction.
That distinction determines whether another attempt is safe.
Deadline expiry triggers cancellation, but stopping work remains cooperative
In gRPC, deadline expiration triggers cancellation of the call.
The signal can propagate through the call chain:
deadline expires
│
▼
RPC cancelled
│
▼
server observes cancellation
│
▼
server stops unnecessary work
The server application still has to cooperate.
In Go:
if err := ctx.Err(); err != nil {
return nil, err
}
Long-running loops can check periodically:
for _, item := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
process(item)
}
Outbound calls should normally use the incoming context so the remaining deadline and cancellation continue downstream:
response, err := dependency.GetProduct(
ctx,
request,
)
Creating a detached background context severs that relationship:
response, err := dependency.GetProduct(
context.Background(),
request,
)
The caller may disappear while detached work continues consuming capacity.
Cancellation cannot erase completed effects
The server may have:
- committed a database transaction;
- sent a message;
- called a payment provider;
- written a file;
before it notices cancellation.
work begins
│
├── commit transaction
│
└── notice cancellation afterwards
The server should stop work that has not yet become necessary or durable.
It cannot automatically reverse completed effects.
If reversal is required, the application needs an explicit compensating operation.
Cancellation means:
The caller is no longer interested in this active interaction.
It does not mean:
Restore every participant to its earlier state.
Expired work should not cascade downstream
Suppose the storefront times out after one second.
Checkout ignores the cancellation and continues.
Inventory continues after checkout gives up.
Payment continues after inventory’s result can no longer be used.
STOREFRONT TIMEOUT
│
▼
checkout still running
│
▼
inventory still running
│
▼
payment still running
The user request is gone.
The system continues performing work for it.
Under load, this creates a reservoir of doomed requests.
Deadline propagation and cooperative cancellation allow downstream services to stop work whose result can no longer be used.
The goal is not merely to return errors sooner.
It is to preserve capacity for operations that can still complete.
A server may reject a clearly unusable remaining budget
Suppose a request arrives with 2 ms remaining and the operation normally requires at least tens of milliseconds.
Beginning expensive work is unlikely to produce a result before the caller disappears.
A server may use conservative admission control:
deadline, ok := ctx.Deadline()
if ok {
remaining := time.Until(deadline)
if remaining < minimumUsefulTime {
return nil, status.Error(
codes.DeadlineExceeded,
"insufficient time remaining",
)
}
}
This threshold should be:
- conservative;
- based on measured behaviour;
- observable;
- revisited as performance changes.
It should not become a permanent universal claim that every invocation requires exactly the same minimum time.
The broader principle is:
Do not begin expensive work when successful delivery is already clearly impossible.
5. When another attempt is safe
A retry is a new attempt
Suppose the first request reaches its deadline.
The client retries.
attempt 1:
reservation committed
response lost
attempt 2:
reservation requested again
Attempt two may overcome a temporary fault.
It may also duplicate completed work.
A retry policy therefore needs two independent judgements:
- Is the failure condition likely to change?
- Is the logical operation safe to attempt again?
A transient-looking status does not automatically make repetition safe.
Retry conditions whose cause may change
Retries can be useful for conditions such as:
- a brief connection interruption;
- one service instance restarting;
- temporary dependency unavailability;
- a database deadlock victim;
- a serialization conflict;
- a temporary rate limit with explicit retry or reset guidance.
Retries are usually useless when the request or relevant state must change:
INVALID_ARGUMENT;PERMISSION_DENIED;FAILED_PRECONDITIONuntil the condition is corrected;NOT_FOUNDfor an identifier whose absence is stable.
NOT_FOUND is not universally permanent. In an eventually consistent system, temporary absence may be part of the documented contract.
Retryability follows the meaning of the operation and error, not the status name alone.
Repeating quantity = 0 does not eventually persuade validation that zero is positive.
Status and retry scope belong together
Different statuses can imply different retry boundaries.
| Status | Typical implication |
|---|---|
UNAVAILABLE |
Repeating the failing call may be appropriate |
ABORTED |
Repeat the larger read-modify-write operation |
FAILED_PRECONDITION |
Change relevant state before retrying |
Suppose an operation performs:
- read the current product revision;
- decide the update;
- write the new value.
If the write returns ABORTED because another writer changed the revision, retrying only the write preserves a decision made from stale state.
The caller must repeat the complete unit:
read current state
│
▼
make decision again
│
▼
attempt complete update
A retry at the wrong level can preserve the failure’s stale assumptions.
Retrieval operations are usually easier to retry
A query such as:
GetProduct chair-42
should not intentionally change authoritative business state.
Another attempt is usually safe.
The result may differ if the product changes between attempts. That is normally acceptable for a current-state query.
State-changing operations are harder:
CreateOrder;CapturePayment;ReserveStock;SendRefund.
The first attempt may already have produced the effect.
Safe repetition needs either natural idempotency or a durable logical-operation identity.
Natural idempotency and operation identity are different tools
A target-state operation can be naturally idempotent:
ensure product 42 is published
Repeating it preserves the same intended state.
An operation such as CreateOrder does not naturally identify which existing order belongs to a repeated request.
It may need a caller-supplied operation ID.
operation_id:
checkout-7f82
The first attempt creates order 5001 and records:
checkout-7f82 → order 5001
The retry uses the same operation ID and receives order 5001 rather than creating order 5002.
ONE LOGICAL OPERATION
│
├── network attempt 1
└── network attempt 2
│
▼
ONE AUTHORITATIVE RESULT
An idempotency contract needs complete rules
“Use the same operation ID” is not enough.
The contract should define:
| Concern | Required decision |
|---|---|
| Scope | Is the ID unique globally, per tenant, per account, or per caller? |
| Retention | How long is the result remembered? |
| Reuse | May the identifier be used again after expiry? |
| Payload matching | Is conflicting reuse rejected? |
| In progress | What happens when another attempt arrives before completion? |
| Result | Is the original response replayed or reconstructed? |
| Authority | Which service owns the operation record? |
If the same identifier is reused with different input, the service should reject the conflict rather than guess which request the caller intended.
Concurrent attempts need one winner
Two attempts can arrive at nearly the same time.
attempt A checks key
attempt B checks key
both see nothing
Without coordination, both may create the business effect.
A duplicate-safe implementation needs a one-winner mechanism such as:
- a unique constraint scoped to the owner and operation ID;
- an atomic in-progress claim;
- conditional insertion;
- locking or serializable coordination;
- a defined response for a duplicate arriving while work is unfinished.
The operation record and business result should be committed atomically where possible.
Otherwise the server can create the effect and crash before recording that the operation ID was consumed.
6. How retries spend the budget
Every attempt and delay consumes the same deadline
Suppose the caller has one second.
Attempt one consumes 400 ms.
Backoff consumes 100 ms.
Only 500 ms remain.
TOTAL DEADLINE: 1,000 ms
│
├── attempt 1: 400 ms
├── backoff: 100 ms
└── remaining: 500 ms
Attempt two cannot receive another full one-second timeout.
Otherwise a one-second operation quietly becomes several seconds before backoff is counted.
No layer may mint more budget.
Do not attempt work that cannot finish usefully
Suppose only 30 ms remain and the dependency normally needs 100 ms.
Another attempt has almost no chance of producing a useful result.
It still consumes:
- connection capacity;
- CPU;
- server queue space;
- telemetry volume.
A retry policy should consider whether enough time remains for:
- connection setup;
- expected processing;
- minimum backoff;
- caller cleanup;
- the response to travel back.
Attempt limits alone do not answer this.
The deadline remains authoritative.
One strong attempt may beat several weak ones
Suppose payment normally needs 300 ms and only 500 ms remain.
A policy might choose:
one attempt with up to 450 ms
instead of:
three attempts with 150 ms each
The shorter attempts may expire during normal processing and create duplicate uncertainty.
More attempts are not automatically more reliable.
The useful allocation depends on:
- normal attempt latency;
- connection reuse;
- probability of transient failure;
- cost of duplicate work;
- idempotency support;
- remaining deadline.
The policy should maximise the chance of a trustworthy result, not the number of attempts.
A bounded retry policy spends time deliberately
A complete retry policy usually includes:
| Element | Purpose |
|---|---|
| Retryable conditions | Limit repetition to failures that may change |
| Maximum attempts | Bound repeated work |
| Initial backoff | Avoid immediate repetition |
| Multiplier | Increase recovery time after repeated failure |
| Maximum backoff | Prevent absurd delays |
| Jitter | Prevent clients retrying together |
| Overall deadline | Bound the complete operation |
| Minimum useful budget | Avoid hopeless attempts |
An exponential schedule might be:
100 ms
200 ms
400 ms
800 ms
Backoff gives a transient condition time to change.
It does not make an unsafe operation idempotent.
Jitter spreads clients across a time window.
WITHOUT JITTER
||||||||||||||||||||
WITH JITTER
| || | | || | |
The purpose is not to make individual clients whimsical.
It is to stop independent clients from becoming one synchronised load generator.
A maximum backoff and overall deadline ensure the schedule does not outlive the value of the work.
Be clear whether the limit counts attempts or retries
A policy may say:
maximum attempts:
3
That normally means:
attempt 1:
original
attempt 2:
first retry
attempt 3:
second retry
Some systems configure “retries” rather than total attempts.
The terminology should be explicit.
Attempt limits protect against persistent outages and misclassification.
They do not replace the complete deadline.
7. Preventing retry-driven overload
Retries multiply across layers
Suppose:
- the client retries service A;
- service A retries service B;
- service B retries the database.
Each layer permits four attempts.
One user operation can create:
4 client-to-A attempts
× 4 A-to-B attempts
× 4 B-to-database attempts
= 64 database attempts
ONE USER ACTION
│
▼
4 attempts
│
▼
16 attempts
│
▼
64 attempts
If the database is already overloaded, this is not resilience.
It is a small denial-of-service system assembled from individually reasonable client libraries.
Retry ownership should be deliberate.
Retry where enough meaning exists
A low-level transport library knows:
- a connection reset occurred;
UNAVAILABLEwas returned;- no response headers were received.
It may not know:
- whether
CreateOrderis idempotent; - whether payment may already be authorised;
- whether state can be reconciled;
- whether the user still needs the result.
A higher application layer understands the business operation.
It may not know whether any request bytes reached the server.
The retry decision belongs where enough information exists to answer:
- Is the condition transient?
- Is the operation repeatable?
- How much deadline remains?
- Will another attempt multiply retries below?
- Can uncertainty be resolved by checking state instead?
For a simple lookup, a client library may be the right owner.
For checkout, the workflow owner may need to control repetition and reconciliation.
Retry budgets protect the whole service
Per-request attempt limits can still create enormous retry traffic during a widespread outage.
Suppose one million requests each permit two retries.
That allows up to two million additional attempts.
A retry budget places a broader limit on retry volume.
For example:
successful first attempts:
10,000 per interval
permitted retries:
1,000 per interval
Once the allowance is exhausted, new requests fail without retrying.
This sacrifices some individual opportunities to protect the system from retry-driven collapse.
Retries should remain a correction to normal traffic, not become the main workload.
A budget can be scoped per process, client, destination, operation or tenant, depending on where overload must be contained.
Overload may require fewer attempts
Suppose a service returns RESOURCE_EXHAUSTED because it lacks capacity.
Immediate retries add work to the resource already failing.
overload
│
▼
errors
│
▼
retries
│
▼
more overload
Recovery may require:
- disabling or reducing retries;
- shedding low-priority traffic;
- reducing concurrency;
- pausing batch work;
- allowing queues to drain;
- restoring capacity.
Retries improve the chance that one request succeeds.
Stopping retries may improve the chance that the service survives.
Server guidance informs timing, not safety
A server may tell callers when another attempt is more appropriate.
For HTTP, Retry-After can accompany responses such as 503 Service Unavailable.
Other APIs can provide reset times or retry delays.
The server knows more about:
- its capacity;
- maintenance;
- rate-limit windows;
- expected recovery;
than the client can infer from one failure.
Server guidance answers part of when.
It does not answer whether the operation is safe to repeat.
Wait-for-ready is not a retry
A gRPC client may wait for a channel to become ready instead of failing immediately when no endpoint is currently available.
That can help during a short deployment transition or service-discovery delay.
The deadline still applies.
wait for ready
│
▼
deadline reached?
│ │
no yes
│ │
▼ ▼
continue fail
Waiting for readiness delays the first usable attempt.
It does not repeat an operation that has already failed.
Once an endpoint is available, the RPC can still return an application or service error.
Connection readiness solves only one part of the interaction.
8. Different work needs different policies
Queued retries still consume capacity
An asynchronous worker may retry by returning a failed item to a queue.
attempt fails
│
▼
delay
│
▼
message redelivered
The caller is no longer waiting synchronously.
The retry still creates:
- another processing attempt;
- another database call;
- another external request;
- more queue storage;
- more operational delay.
A poison message can cycle indefinitely.
A worker needs:
- attempt count;
- backoff;
- maximum age;
- a failed-work state;
- operator-visible ownership;
- a safe replay procedure.
Asynchrony moves the waiting.
It does not make repeated work free.
Long-running work needs its own lifecycle
Suppose report generation normally takes 20 minutes.
One synchronous RPC with a 30-minute deadline requires the client, server, network path and intermediaries to remain useful for the entire period.
A better contract may create an operation resource.
rpc StartReport(StartReportRequest)
returns (StartReportResponse);
message StartReportResponse {
string operation_id = 1;
}
The client later queries or subscribes to a state such as:
- pending;
- running;
- completed;
- failed.
SUBMISSION DEADLINE
How long may the caller wait
for the job to be accepted?
PROCESSING DEADLINE
How long may report generation continue?
OBSERVATION
How does the caller learn the result?
The same shape can be implemented through HTTP, RPC or messaging.
One enormous timeout is often evidence that the work needs its own lifecycle.
Each dependency needs a policy that matches its semantics
A checkout operation might contain:
| Dependency | Typical policy |
|---|---|
| Catalog lookup | Short, read-only, perhaps one bounded retry |
| Inventory reservation | Stable operation ID and queryable reservation state |
| Payment authorisation | Provider idempotency and reconciliation for unknown outcomes |
| Notification publication | Durable record and asynchronous delivery |
| Order transaction | Retry the complete local transaction on eligible conflicts |
One generic interceptor cannot safely decide every policy.
Shared tooling can implement a policy consistently only after the operation’s meaning is known.
A checkout budget is allocated dynamically
Suppose the storefront gives checkout 1,500 ms.
Checkout reserves:
- 150 ms for validation and basket loading;
- 200 ms for final order commit and response;
- 150 ms as a safety allowance.
That leaves 1,000 ms for remote dependencies.
A rough plan might be:
| Dependency | Planned maximum |
|---|---|
| Catalog | 200 ms |
| Inventory | 300 ms |
| Payment | 500 ms |
Actual limits still come from the remaining deadline when each call begins.
If payment starts with only 360 ms left, it cannot use the originally planned 500 ms.
effective payment limit:
minimum of
configured payment limit
and
remaining checkout budget
The parent deadline remains the outer boundary.
The complete checkout policy contains several recovery paths
For a bfstore checkout operation:
OUTER OPERATION
PlaceOrder
operation_id:
checkout-7f82
Catalog lookup
- Purpose: validate current product facts.
- Retry: one bounded retry on a suitable transient failure.
- Idempotency: retrieval operation.
- Fallback: do not accept stale price for final order creation.
Inventory reservation
- Purpose: reserve the requested stock.
- Retry: only with a stable reservation operation ID.
- Unknown outcome: query reservation state by that identity.
- Deadline: child of the checkout deadline.
Payment authorisation
- Purpose: obtain a payment decision.
- Retry: only under the provider’s idempotency contract.
- Unknown outcome: record pending and reconcile.
- Deadline: preserve enough time to commit or report a truthful pending state.
Order commit
- Purpose: persist the accepted order.
- Retry: rerun the complete local transaction on eligible deadlock or serialization failure.
- External effects: record them through a durable outbox rather than performing them inside the retriable transaction.
Notification
- Purpose: tell the customer about a committed order.
- Critical path: no.
- Retry: asynchronous delayed attempts.
- Failure: operator-visible failed-work state.
One global setting such as:
timeout = 5 seconds
retries = 3
would erase the distinctions that make each operation safe.
9. Observe, test and review the policy
Retries can hide dependency degradation
Suppose attempt one fails and attempt two succeeds.
The user receives success.
Without attempt-level metrics, the system appears healthy.
But a rising attempt count means:
- more dependency traffic;
- more latency;
- more consumed deadline;
- less resilience during further degradation.
The system should measure both logical operations and network attempts.
logical operations:
1,000
successful operations:
995
total attempts:
1,480
The final success rate looks strong.
The attempt multiplier is signalling trouble.
Measure the complete policy
Useful measurements include:
- logical-operation count;
- attempt count;
- attempts per operation;
- retry count and success rate;
- retry-budget exhaustion;
- timeouts by operation;
DEADLINE_EXCEEDEDresponses;- cancellations observed by servers;
- remaining budget on arrival;
- backoff duration;
- dependency latency per attempt;
- final operation latency;
- unknown outcomes;
- idempotency-key replays and conflicts.
These answer different questions.
| Measurement | Question |
|---|---|
| Operation success rate | Did the caller eventually receive a usable result? |
| Attempt success rate | How often does one network attempt work? |
| Retry effectiveness | How many retries convert failure into success? |
| Retry cost | How much extra traffic do retries create? |
| Deadline pressure | How often does work arrive with too little time left? |
A retry policy should be observable as a workload, not hidden inside a client stub.
Logs need operation and attempt identity
A state-changing operation may have:
operation ID:
checkout-7f82
Each network attempt should also have its own request identity.
checkout-7f82
│
├── request req-91a3
├── request req-b102
└── request req-c447
Logs can then record:
- logical operation;
- attempt number;
- request ID;
- remaining deadline;
- previous status;
- chosen backoff;
- idempotency result.
This distinguishes one operation retried three times from three separate customer operations.
Test realistic failure windows
Useful controlled failures include:
- dropping the request before the server receives it;
- delaying the server before processing;
- committing a transaction and then losing the response;
- sending two attempts with the same operation ID concurrently;
- returning
UNAVAILABLEbefore work begins; - returning
ABORTEDafter a concurrency conflict; - expiring the deadline during processing;
- cancelling after work begins;
- making many clients retry together;
- exhausting the retry budget;
- restarting services with cold connections;
- sending response bytes slowly enough to reset read timeouts.
Tests should verify:
- whether another attempt occurs;
- which operation identity it uses;
- whether one winner is enforced;
- whether duplicate effects are prevented;
- whether expired work stops;
- whether backoff and jitter are applied;
- whether metrics identify retry traffic;
- whether the final state can be reconciled.
A retry policy is not proven by a unit test showing that a loop executes three times.
It is proven by the final state left after realistic failure timing.
A practical policy table
| Operation | Deadline approach | Retry approach | Additional requirement |
|---|---|---|---|
| Read product | Short caller-defined deadline | Bounded retry on suitable transient failure | Current state may change between attempts |
| Search products | Short or moderate deadline | Possibly retry before response commitment | Pagination and partial-result policy |
| Reserve stock | Fits checkout deadline | Stable operation ID | Queryable reservation state |
| Create order | Fits checkout deadline | Duplicate-safe retry | Durable idempotency record |
| Authorise payment | Strict workflow deadline | Provider-specific idempotency | Reconciliation for unknown outcomes |
| Generate report | Short submission deadline | Background processing attempts | Durable operation resource |
| Deliver notification | Independent worker deadline | Delayed attempts | Deduplication and failed-work process |
| Database transaction | Local transaction deadline | Retry complete transaction on eligible abort | No uncontrolled external side effects |
| Cache lookup | Very small sub-deadline | Usually little or no retry | Safe fallback to source |
| Administrative command | Caller- and operation-specific | Often no automatic retry | Clear operator-visible outcome |
The table does not define universal durations.
It identifies which semantics must accompany the numbers.
A compact policy checklist
| Concern | Question |
|---|---|
| Purpose | When does the result stop being useful? |
| Outer deadline | What bounds the complete business operation? |
| Propagation | Does every downstream call receive the remaining budget? |
| Local limit | Does this dependency need less time than the parent permits? |
| Setup | Does the configured limit include DNS, connection and TLS work? |
| Total interaction | Could per-read timers allow a slow trickle to continue too long? |
| Cancellation | Will downstream work stop when the caller leaves? |
| Ambiguity | Could the operation commit before expiry is observed? |
| Retryability | Which conditions may change without changing the request? |
| Idempotency | Can repeated or concurrent attempts create another effect? |
| Scope | Which layer understands enough to own the retry? |
| Attempts | How many total attempts are permitted? |
| Backoff | How much time should pass between attempts? |
| Jitter | How are many callers prevented from retrying together? |
| Capacity | How much global retry traffic is acceptable? |
| Recovery | How is an unknown outcome queried or reconciled? |
| Evidence | Can operations and attempts be observed separately? |
A timeout value without these answers is only a number attached to uncertainty.
The mental model I am keeping
My original model was:
call dependency
│
▼
wait for timeout
│
▼
retry three times
The stronger model is:
BUSINESS OPERATION
│
▼
OUTER DEADLINE
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
attempt 1 backoff attempt 2
│ │ │
└────────────────┼────────────────┘
▼
REMAINING BUDGET
│
┌────────────┴────────────┐
│ │
▼ ▼
known final result uncertain outcome
│ │
▼ ▼
continue reconcile state
The deadline answers:
How long does this result remain useful?
A local timeout answers:
How much of that budget may this wait consume?
A retry policy answers:
Which unsuccessful attempts deserve another attempt, at which layer and under which business identity?
Backoff answers:
How do I give a transient condition time to change?
Jitter answers:
How do many callers avoid retrying together?
Idempotency answers:
How do several attempts remain one business effect?
A retry budget answers:
How do individual retries avoid damaging the health of the whole service?
And reconciliation answers the question retries cannot always resolve:
What actually happened?
These mechanisms are useful when they preserve one finite, honest budget.
A caller begins with a limited amount of useful time.
Every network call, backoff interval and retry spends some of it.
No layer may quietly mint more.
No retry may assume the earlier attempt did nothing.
No timeout may claim that remote state was rolled back.
Timeouts, deadlines and retries are not generic resilience switches.
They are parts of an operation’s contract:
- how long the result remains useful;
- how uncertainty is represented;
- how repetition is controlled;
- how overload is contained;
- how the final state becomes known.
Used carefully, they stop one slow or unavailable dependency from trapping the entire application.
Used carelessly, they turn one failing request into a long-running family of failing requests, all arriving with excellent intentions and no remaining budget.
References and further reading
gRPC deadlines, cancellation and readiness
gRPC: Deadlines
Explains client deadlines, server behaviour, propagation of remaining time and clock-skew handling.
gRPC: Cancellation
Explains cancellation caused by client action, deadline expiry or I/O failure, and the application’s responsibility to stop ongoing work.
gRPC: Wait-for-Ready
Explains how a call can wait for channel readiness while remaining bounded by its deadline.
gRPC and Deadlines
Discusses why remote calls need deadlines and why clients and servers can reach different conclusions about the same call.
gRPC retries and statuses
gRPC: Retry
Describes retry attempts, retryable status codes, attempt limits, exponential backoff and throttling.
gRPC: Client-Side Retry Design
Defines the gRPC retry model, commitment, backoff, throttling and server pushback.
gRPC: Status Codes
Defines statuses including DEADLINE_EXCEEDED, UNAVAILABLE, ABORTED, FAILED_PRECONDITION and RESOURCE_EXHAUSTED.
Retry safety and overload
Amazon Builders’ Library: Timeouts, retries, and backoff with jitter
Discusses timeout selection, false timeouts, connection setup, retry amplification, backoff, jitter and retry control.
Amazon Builders’ Library: Making retries safe with idempotent APIs
Explains caller-provided operation identifiers, duplicate-safe processing and conflicting reuse.
AWS Architecture Blog: Exponential Backoff and Jitter
Demonstrates how jitter reduces synchronised attempts and total contention.
Google SRE Book: Addressing Cascading Failures
Explains retry amplification, retry budgets, load shedding and containment of cascading failure.
HTTP retry semantics
RFC 9110: HTTP Semantics
Defines HTTP method semantics, idempotency and Retry-After.
RFC 9112: HTTP/1.1
Defines HTTP/1.1 messaging, connection management and incomplete-message handling.
These specifications supersede RFC 7231 and the relevant portions of RFC 7230, which were current when the original article was written.