keepcalculator

06 Numbers and Powers

Cancel in Progress: What GitHub Actions Does and Doesn’t Guarantee

Card No. 06·63171 · Updated · Standard formula, shown below

When you cancel a workflow run in GitHub Actions, GitHub sends a cancellation signal to every job that is still running, stops starting new jobs, and marks the run as cancelled — but it does not guarantee that running steps will stop immediately, that already-finished jobs will be reverted, or that background processes spawned by your steps will be terminated. Cancellation is a best-effort cooperative process: each step must check for the cancellation signal and exit on its own, and GitHub gives it a grace period before forcibly killing the process.

Background: Why Cancellation Is Not Instant

GitHub Actions runs each job in a fresh virtual machine or container. When you click “Cancel workflow” (or use the API, or a workflow_dispatch cancellation), GitHub sends a SIGINT/SIGTERM-like signal to the primary process of each active step. The runner then waits for the process to exit. If it doesn’t exit within a timeout (default 5 minutes for a step, but the cancellation grace period is typically 30 seconds to 5 minutes depending on the runner), the runner sends SIGKILL. This means a long-running npm test or sleep 1000 will not vanish instantly — it will keep running until it handles the signal or the grace period expires.

How Cancellation Works in Practice

When you cancel a run, here’s what happens step by step:

  • GitHub marks the run as cancelled and stops queued jobs from starting.
  • For each in-progress job, the runner sends a cancellation signal to the active step’s process.
  • If the step’s script handles the signal (e.g., a shell trap or a program that listens for SIGINT), it can clean up and exit. If not, the runner waits for the grace period.
  • After the grace period, the runner kills the process group with SIGKILL.
  • Steps that were waiting in the queue (e.g., a needs dependency that hasn’t started) are simply never started.
  • Artifacts and logs from already-completed steps are preserved; they are not rolled back.

Importantly, cancellation does not run any “cleanup” steps. If you have a post: step or a finally-like pattern using if: always(), those will still run after cancellation, but only if the job itself is not killed. If the job is forcibly killed, even always() steps may not run.

What Cancellation Does NOT Guarantee

  • No immediate stop: Steps that ignore signals will run until the grace period ends.
  • No rollback of side effects: If a step already pushed a commit, uploaded an artifact, or sent a webhook, those actions remain. Cancellation doesn’t undo them.
  • No guarantee that background processes die: A step that starts a background process (e.g., nohup ./server &) may leave it running after the step is killed, because the runner kills the process group but not necessarily detached processes.
  • No guarantee that dependent jobs are cancelled in order: If Job B depends on Job A, and Job A is cancelled, Job B will be skipped (marked as skipped), but if Job B already started (e.g., via needs with if: always()), it may continue.
  • No guarantee that the cancellation signal is delivered to every process: Only the main process of the step receives the signal; child processes may not.

How to Handle Cancellation Gracefully

If you want your workflow to respond to cancellation cleanly, you can:

  • Use a trap in bash to catch SIGINT/SIGTERM and run cleanup commands.
  • Add a final step with if: always() to clean up resources, but note it may not run if the job is killed.
  • Use the timeout-minutes on steps to limit their runtime, so they don’t hang forever.
  • For long-running processes, check for cancellation by polling the GitHub API or using the cancelled() context in conditionals.
  • Avoid background processes that outlive the step; if you must, use disown or a process manager and ensure they are killed in a cleanup step.

Common Misconceptions

One common misconception is that cancelling a run will “stop everything immediately.” In reality, a step that is in the middle of a docker build or a large download will keep going until it finishes or is killed. Another is that cancelled runs release their resources instantly — they don’t; the runner may take time to clean up the VM. Also, many users assume that if they cancel a run, any artifacts already uploaded will be deleted — they are not, they remain attached to the cancelled run.

Comparing with Other CI Systems

GitHub Actions’ cancellation behavior is similar to GitLab CI’s, which also sends a SIGINT and waits for a grace period. Jenkins, on the other hand, by default aborts the build immediately (SIGKILL) unless you configure an abort hook. Travis CI historically killed the build after a short delay. The key difference is that GitHub Actions gives you a chance to clean up, but only if you implement it.

Cost and Resource Implications

Cancelling a run does not refund any compute minutes already used. You are billed for the time the job ran until it was cancelled, not for the full timeout. However, if a job is stuck and you cancel it, you avoid paying for the remaining time. For self-hosted runners, cancellation frees the runner for other jobs, but the runner may still be busy until the grace period ends.

Safety and Best Practices

  • Always set timeout-minutes on jobs to prevent runaway processes.
  • If you have steps that modify external systems, make them idempotent so that re-running after a cancellation doesn’t cause duplicates.
  • Use concurrency groups to automatically cancel older runs when a new one starts — this is a common pattern for deployments.
  • For critical cleanup (e.g., stopping a database), use a separate job with if: always() and needs that runs even if the main job is cancelled.
  • Test your cancellation handling by actually cancelling a run in a staging environment.

Frequently Asked Questions

Will cancelling a run stop a step that is already running?

It sends a signal, but the step must handle it. If the step ignores the signal, it will run until the grace period (usually a few minutes) then be killed. So it’s not immediate.

Can I run cleanup code when a workflow is cancelled?

Yes, you can use a step with if: always() or if: cancelled() to run cleanup. However, if the job itself is forcibly killed, that step may not run. For guaranteed cleanup, use a separate job that depends on the main job with if: always().

Does cancelling a workflow delete artifacts that were already uploaded?

No. Artifacts uploaded before cancellation remain attached to the run. You can delete them manually or via the API.

What happens to jobs that haven’t started yet when I cancel a run?

They are marked as skipped and never start. If they have if: always(), they may still run, but that’s rare.

Does cancellation affect the billing for the run?

You are billed for the compute time actually used up to the point of cancellation. You don’t pay for the full timeout.

How do I automatically cancel previous runs when a new one starts?

Use the concurrency key in your workflow with group and cancel-in-progress: true. This is a common pattern for deployment workflows.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →