cronalerts all systems nominal

Retries that hide the original failure

A successful retry can erase the first failure from dashboards and alerts. Keep attempt history visible so late greens do not hide standing defects.

A job fails at 02:14. The framework retries at 02:17 and succeeds. By morning the dashboard shows a green run, the exit code is zero, and nobody opens a ticket. The original failure still happened. The data was late, the lock was held longer than expected, the upstream API returned a 503 for three minutes, or the worker ran out of memory on the first attempt. None of that appears in the success story the scheduler tells you.

Retries are useful. They are also a filter. Every automatic retry that eventually succeeds removes a failure from the place where operators look. Over months, teams train themselves to trust the final status of a run and stop asking what the first attempt looked like. That habit is fine until the first attempt is the only one that carries the signal you needed.

What a successful retry erases

Most batch frameworks record the outcome of the last attempt as the outcome of the job. Celery marks the task successful. Sidekiq reports a completed job. A Kubernetes CronJob pod exits zero on the second try and the Job object looks healthy. The retry history may live in a log line or a nested event, but it is rarely promoted to the same surface as the final status. Operators scan green and red. They do not scan "green after two failures."

The erased information is not cosmetic. The first failure often carries the cause. A connection timeout, a unique-constraint violation, a missing file, a permission error on a rotated credential: those details sit on attempt one. Attempt two may succeed because the lock cleared, the file arrived late, or the rate limit window reset. The root cause is still in production. You just stopped receiving it as an alert.

Timing is the other casualty. A nightly export that should finish by 03:00 can still "succeed" at 04:40 after four retries. Downstream jobs that assumed a 03:15 availability window run against stale input or skip. From the scheduler's point of view the export worked. From the business's point of view the day started on yesterday's data. If your monitoring only asks whether the job eventually exited zero, late success and on-time success look identical.

I have seen teams celebrate a drop in failed-job counts after they raised the retry ceiling. The failure rate did not drop. The classification changed. Errors that used to page now resolve themselves inside the retry loop, and the metrics that leadership reviews treat that as improvement. It is not improvement. It is a quieter failure mode with a longer fuse.

Intermittent resource pressure is the classic case. A worker pool that is slightly undersized fails under concurrent load on the first attempt, then succeeds when contention eases. The retry hides the capacity problem. Capacity problems that only show up as first-attempt failures never appear in the weekly reliability report, because the report reads final status.

Transient dependency outages behave the same way. An upstream that flakes for five minutes during your window will generate a cascade of first-attempt errors and a cascade of second-attempt successes. If you only alert on terminal failure, you learn about the upstream when it stays down longer than your retry budget. By then you have already spent the budget without knowing you were spending it.

Partial success is harder. Some jobs retry the whole unit of work after a mid-run failure. The first attempt wrote half the rows, rolled back incompletely, or left a side effect in a third-party system. The second attempt "succeeds" against a messier world than the code assumed. Final status is green. The data is wrong. Retries did not hide a transient blip; they hid a non-idempotent path that should never have been retried blindly.

Duplicate side effects belong in the same family. A payment notification, a webhook, or an email send that fires on attempt one and again on attempt two will not show up as a failed job. It shows up as a customer complaint or a double charge. The monitoring question "did the job fail?" answers no. The operational question "did the job do the work once?" never got asked.

Keep the first failure visible

Treat attempts as first-class events, not footnotes. Emit a metric or log field for attempt number and for first-attempt failure rate, separate from terminal failure rate. A job that terminals green ninety-nine percent of the time but fails on the first attempt thirty percent of the time is telling you something. Put that rate next to the success rate on the same dashboard. If you only chart terminal outcomes, you will optimize for the chart and miss the signal.

Cap retries with intent, not habit. Three retries with exponential backoff is a reasonable default for network blips. It is a poor default for a job that must finish inside a fixed window, or for a job whose failure mode is usually a permanent data problem. Permanent failures should fail fast so a human sees them. Infinite or high-ceiling retries convert permanent failures into delayed pages, which is worse than an immediate page because the on-call person arrives after the SLA has already burned.

Alert on late success when lateness matters. If the job has a business deadline, a success after the deadline is a different event from a success before it. Record completion time against the expected window. A green run that finished forty minutes late should not share a status with a green run that finished on time. Heartbeat-style monitors that expect a check-in by a certain minute catch this class of problem even when the process eventually exits zero. That is closer to the signal you want than a terminal exit code alone.

Preserve the original error. When a framework retries, make sure attempt-one exceptions land in durable storage with the same job identity as the eventual success. On-call should be able to open a run and see "attempt 1: connection refused to billing-db; attempt 2: success" without spelunking three log systems. If that history is only available during the incident window and then rolls off, you will not notice rising first-attempt failure rates until someone asks why the dependency has been flaky for a quarter.

Make idempotency a precondition for retry, not an afterthought. If the job is not safe to run twice, do not configure automatic retries. Prefer a dead-letter path and an explicit requeue after inspection. Blind retries on non-idempotent work trade a visible failure for a silent double-write. Visible failures are cheaper.

What to change this week

Pick one critical scheduled job and pull its attempt history for the last thirty days, not just its final statuses. Count how often the first attempt failed and a later attempt succeeded. Look at the error classes on those first attempts. If you see the same timeout, lock, or credential error repeating, you have a standing defect that your retry policy has been absorbing. Fix the defect, then decide whether the retry budget still makes sense.

If the job has no attempt history you can query, that is the first fix. You cannot manage what the platform discards. Add structured fields for attempt number, error class, and duration per attempt. Wire a simple alert on first-attempt failure rate crossing a threshold you would actually act on. Keep the terminal-failure alert too. You need both.

Retries buy you resilience against noise. They also buy you blindness to the noise's source. The teams that sleep better are not the ones with the highest retry ceilings. They are the ones who still see the first failure, even when the second attempt saves the run.