Skip to content

Self-hosted runner agents die mid-job, presenting as test failures (3x in one evening) #834

Description

@ejc3

Three CI jobs failed on 2026-08-14/15 with no failing test. All three were runner-agent deaths mid-job. Each cost a 20-40 minute job and, because the failure looks like a code failure, an investigation.

Signature (identical in all three):

  • The job log stops mid-stream while the job keeps running for another 15-20 minutes.
  • The test-root step never receives a conclusion; every later step stays pending (.steps[].conclusion == null), including "Upload test logs" — so the test-logs-* artifact is MISSING for exactly the failed job while every sibling job in the same run uploaded one.
  • No TIMEOUT line, and tests were completing normally right up to the last emitted line. CI's nextest profile terminates any test after 2x120s and reports it, so a hung test would have been named.
  • GitHub eventually marks the job failure.

Evidence per occurrence (all on PR #818's head 2917e07, three different machines):

job runner duration instance state after
Host-Root-x64-SnapshotDisabled runner-i-0d74c773f1e998a00 17:20:53 -> 18:00:57 terminated (gone from EC2)
Host-Root-x64-SnapshotDisabled runner-i-0f0956aaa6475b7c6 18:23:44 -> 18:43:46 terminated (gone from EC2)
Host-Root-arm64-SnapshotDisabled runner-i-02b10d79333d05c37 23:58:35 -> 00:32:38 instance still running, agent status=offline

The third case is the clearest: the EC2 instance is alive and the GitHub runner agent is simply gone. SSH to it also times out from an allowlisted IP, so the box is not healthy either.

Confirmation it is not the code: the same commit, rerun on a fresh runner, passed 15/15 with every required job present, including the Host-Root-arm64-SnapshotDisabled job that had died.

Why it concentrates on SnapshotDisabled: that configuration cold-boots every VM, making it by far the longest and most VM-heavy job, so it has the widest window to be caught by whatever kills the agent (scale-down/lease reclaim, or the host wedging under load).

Why it matters beyond the lost time: the failure is indistinguishable from a code failure at a glance, and it stops the run before the diagnostic artifact is uploaded — so the evidence needed to tell the two apart is exactly what gets destroyed. This session spent a long investigation, and produced one wrong attribution to a PR, before the runner data settled it.

Suggested directions (autoscaler lives in ~/aws/runner-autoscale.tf):

  • Do not reclaim/terminate an instance whose runner reports busy=true; if a lease must expire, drain rather than kill.
  • Emit a distinguishable signal when the agent dies mid-job (a post-job check, or an annotation) so a dead agent never reads as a test failure.
  • Consider uploading test logs incrementally, or on a trap, so a killed job still leaves its diagnostics.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions