Effect · AI in production
The AI call timed out. Should I retry it?
How I use Effect to separate safe retries, uncertain results and work that needs a person.
A timeout leaves a question open.
An agent sends a request. Nothing comes back. Should it try again?
If it was reading an auction listing, probably. If it was adding a lead to a CRM, I’d first want to know whether the lead was already added.
The timeout tells me I didn’t get an answer. It doesn’t tell me nothing happened.
That’s where I find Effect useful: I can give different failures different policies, rather than wrap the whole job in a retry and hope for the best.
Separate a bad answer from a failed request.
For a lead-research workflow, I’d distinguish a few situations:
The source is temporarily unavailable. A bounded retry may help.
The response cannot be decoded. Investigate the response or use a deliberate repair step with its own limit.
The listing is valid but unsuitable. Record why it was rejected. Asking again won’t change the buyer’s budget.
A write has an unknown outcome. Check the receiving system before attempting it again.
Effect’s typed error channel gives expected failures a name and a place in the function signature. It doesn’t mean every possible bug is now in that type. Defects and interruption still need proper handling at the application boundary.
Keep the retry around the read.
Here’s a small policy using Effect v4. lookup represents a read-only provider call. Its adapter has already classified temporary failures.
import { Effect, Schedule } from 'effect';
type LookupError = { readonly _tag: 'LookupUnavailable' } | { readonly _tag: 'InvalidListing' };
declare const lookup: Effect.Effect<string, LookupError>;
const retryPolicy = Schedule.max([Schedule.exponential('250 millis'), Schedule.recurs(2)]).pipe(
Schedule.setInputType<LookupError>(),
Schedule.while(({ input }) => input._tag === 'LookupUnavailable'),
);
const readListing = lookup.pipe(Effect.retry(retryPolicy), Effect.timeout('5 seconds'));That permits the first attempt plus at most two retries. The timeout sits outside the retry, so it covers the read attempts and the waiting between them. Moving it inside would give each attempt a separate timeout budget.
Those timings are examples, not production defaults. I’d choose them around the provider’s limits and how long the caller can reasonably wait. For concurrent callers, I’d also consider jitter and respect any retry delay supplied by the provider.
Interruption needs cooperation from the underlying client to cancel the network request. It cannot undo a remote action that has already happened.
Make writes safe separately.
For a CRM write, I’d use an idempotency key that stays the same across attempts, if the receiving API supports it. Generating a fresh key on each retry defeats the point.
If the API cannot deduplicate requests, I need another recovery path: perhaps looking up the operation by a stable reference, or flagging it for review. A local flag alone cannot prove what happened remotely.
For a job that must survive a restart, I’d persist progress and use durable execution where it earns its place. Even then, the external write needs a duplication policy. Persistence doesn’t give another company’s API exactly-once behaviour.
Check usefulness, not just successful execution.
I’d test temporary outages, malformed responses, cancellation and uncertain writes with controlled service implementations. Then I’d evaluate the actual shortlist with the person doing the research.
Can they trace the details to a source? How many leads do they keep? How much checking is left?
A schema can validate a date. It can’t establish that the auction house published the right one. I want the system to make that uncertainty visible, so the operator can spend less time playing detective.
Decide what each failure means before deciding to retry it. Effect makes that policy explicit, but the business rules still need a person to think them through.
Working on something similar?
Get in touch