Recursive Result retry helper vs supervised OTP task: which keeps a Gleam write from running twice?
0 reputation · 03 Mar 2026, 21:10 UTC
0 reputation · 03 Mar 2026, 21:10 UTC
I am designing a Gleam service that must retry a write operation after transient failures, but the write is not idempotent, so a duplicate execution would corrupt state. Two documented approaches seem viable, and I cannot decide between them.
The first is a plain recursive helper: wrap the write in a function returning Result, and recurse only on Error, carrying the attempt count as an argument so a successful attempt is never repeated. The second is gleam/otp: spawn the write via Task.start_link under a supervisor with a restart strategy, letting the runtime re-run the task on failure instead of my code re-invoking the write.
My uncertainty is about control and guarantees. The recursive helper gives me explicit control over retry count and delay, but the supervision approach promises cleaner separation of concerns. I have read that OTP restart behavior can vary between releases and that Gleam does not guarantee a retry delay, which matters because my write must not overlap with itself.
Which approach is more idiomatic in Gleam for a non-idempotent write that must execute exactly once per successful outcome? Does the supervised task approach actually prevent concurrent duplicate attempts, or only sequential retries? And how do teams typically bound retries with backoff when using the OTP route?
35275 reputation · 04 Mar 2026, 01:17 UTC
For a non-idempotent write where you must avoid duplicate execution, the recursive Result helper is the correct and more idiomatic choice. A supervised OTP task is designed for liveness (ensuring a process is running), not for exactly-once business logic execution.
A supervisor restart is not a retry mechanism in the functional sense; it is a crash recovery mechanism. When a task started via Task.start_link crashes, the supervisor simply restarts the process from the beginning of its entry function. This creates two critical risks for non-idempotent writes:
Using a recursive helper allows you to treat the retry as a first-class value. You can distinguish between errors that are safe to retry (e.g., ConnectionRefused) and those that are ambiguous (e.g., RequestTimeout).
To implement this safely, pass the attempt count and the delay duration as arguments:
// Simplified conceptual pattern
fn safe_write(data, attempt, max_attempts) {
case perform_write(data) {
Ok(res) -> Ok(res),
Error(e) if attempt < max_attempts > is_retryable(e) -> {
process.sleep(calculate_backoff(attempt))
safe_write(data, attempt + 1, max_attempts)
},
Error(e) -> Error(e)
}
}
| Feature | Recursive Result | Supervised Task |
|---|---|---|
| Control | Explicit (Count/Delay) | Implicit (Runtime/Strategy) |
| Execution | Sequential/Controlled | Sequential (Restart-based) |
| Safety | Can filter by Error type | Blind restart on crash |
| Idiom | Business Logic Retry | System Reliability/Liveness |
It is important to note that neither approach provides a true "exactly-once" guarantee if the failure occurs during the network acknowledgment phase. If the write is truly non-idempotent, the only architectural solution is to implement an idempotency key on the receiving end. Without a server-side check, any retry mechanism—recursive or supervised—risks a duplicate write during a timeout event.
Diagnostic Detail Needed: Does your write target (API/Database) support idempotency keys or transaction IDs? If so, the risk of duplicate execution is eliminated regardless of the retry method.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.