What changed
We overhauled how CircleCI handles workflow cancellation to eliminate two long-standing issues: jobs that flip from canceled to success, and jobs that keep showing a spinner after they’ve actually finished.
Background
Previously, when a workflow was canceled, CircleCI would eagerly mark all jobs as canceled — before those jobs had actually stopped running. This caused two visible problems:
- Status flipping: A job marked canceled may have already finished running. The actual confirmation would then arrive with the actual status and overwrite the result. Users would see jobs flip
canceled → success. - Phantom spinners: Jobs were marked
canceledand archived before receiving confirmation and an end timestamp. The UI interprets a job with a start time but no end time as still running, so completed jobs kept spinning indefinitely.
The fix
CircleCI no longer marks jobs canceled until they’ve actually stopped. Instead:
- It records a “canceling” inten
- It sends cancel signals only to jobs that are
pendingorrunning. - It waits for each job to confirm it has stopped before recording the final status.
Downstream blocked jobs are left untouched and transition to skipped / not_run through the normal state engine, as expected.
Impact
- Canceled jobs with a terminal end time: ~51% → 99%+
- No more
canceled → successstatus flips - No more phantom spinners for jobs that have already finished
What’s next
We’re migrating the single-job cancel path to the same defer-to-:job-ended model. This closes the remaining gap where individually canceled jobs can still report a start time but no end time.