Patterns: concurrent Go · 3 MIN READ
Retry: bounded attempts and idempotent effects
Retry transient failures within one deadline without multiplying side effects.
Retry transient failures within one deadline without multiplying side effects. A retry repeats an operation that may already have succeeded remotely. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.
Concurrent Go pattern · A MAP OF THE IDEA
- Classify a transient failure
- Wait within a shared deadline
- Retry the same logical idempotent operation
Useful when
- A transient failure can plausibly clear
- The operation is safe to repeat within a budget
Watch out
- Retries can multiply load and duplicate writes
- Backoff must be capped and jittered in production
Implementation & operation cost: High
Fit depends on the task
Editorial suitability, not benchmarks or a universal ranking. Styles describe different axes and can be combined.
The design decision
A retry repeats an operation that may already have succeeded remotely. Limit attempts and total time, classify transient failures, and use an idempotency key for nontrivial writes. Backoff spaces attempts; jitter in production reduces synchronized retry bursts across clients.
A complete Go example
Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.
package main
import (
"context"
"errors"
"fmt"
"time"
)
var ErrTemporary = errors.New("temporary failure")
func Retry(ctx context.Context, attempts int, op func() error) error {
if attempts <= 0 || attempts > 8 {
return errors.New("attempt limit must be 1 to 8")
}
for attempt := 0; attempt < attempts; attempt++ {
if err := ctx.Err(); err != nil {
return err
}
err := op()
if err == nil || !errors.Is(err, ErrTemporary) || attempt == attempts-1 {
return err
}
timer := time.NewTimer(time.Millisecond * time.Duration(1<<attempt))
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
}
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
calls := 0
err := Retry(ctx, 3, func() error {
calls++
if calls < 3 {
return ErrTemporary
}
return nil
})
fmt.Println(calls, err)
}
// Output:
// 3 <nil>
The operation succeeds on attempt three; permanent errors would stop immediately. The backoff wait observes cancellation. The supplied operation is local and immediate; a real remote operation should accept the same context so the deadline also reaches I/O.
Tradeoffs and the simpler alternative
Keep attempt limits small and cap exponential delay before shifts can overflow. This mechanism omits jitter for deterministic teaching. Never blindly retry a payment without a durable idempotency contract, and avoid nested retry layers that multiply traffic during an outage.
TAKE THIS WITH YOU
Needs bounded attempts and an idempotent effect contract. Keep attempt limits small and cap exponential delay before shifts can overflow. This mechanism omits jitter for deterministic teaching. Never blindly retry a payment without a durable idempotency contract, and avoid nested retry layers that multiply traffic during an outage.
Check your understanding
Which statement best describes Retry: bounded attempts and idempotent effects?
Go to the source
Progress stays in this browser. No account needed.