Dissecting the GitHub Actions Outage: When Proactive Fixes Meet Hidden Blind Spots

Arpit Bhayani

Arpit Bhayani

Jun 10, 2022 • 7 min read

Play

Dissecting the GitHub Actions Outage: When Proactive Fixes Meet Hidden Blind Spots

Anticipating a failure mode months in advance and executing a proactive mitigation is the gold standard of systems engineering. Yet, on March 1, 2021, GitHub suffered a widespread outage on its Actions service—due to an exact failure mode the engineering team had identified, planned for, and migrated six months prior.

This incident provides a masterclass in distributed systems engineering, the limits of system observability, and how third-party dependencies can introduce latent single points of failure (SPOFs) that bypass standard architectural safeguards.


1. Context: GitHub Actions and High-Frequency ID Generation

GitHub Actions handles continuous integration and continuous deployment (CI/CD) workflows triggered by developer actions—primarily code pushes, pull requests, and commit updates. For every event, multiple tasks and validations run automatically (e.g., unit tests, linters, secret scanners, code coverage).

The Check Suite Lifecycle

Before any workflow job begins execution, GitHub encapsulates the group of checks inside an entity known as a Check Suite (and corresponding Check Runs).

  1. A developer pushes a commit or opens a pull request.
  2. The system creates a check_suite row entry in GitHub’s relational database (predominantly MySQL).
  3. Each check suite is assigned a unique, monotonically increasing auto-incrementing primary key ID.
  4. A puller/orchestration service reads the newly created check suite and queues jobs onto an execution queue.
  5. Worker nodes (runners/executors) pull from the queue and run the predefined actions.
  6. Statuses are reported back, and the PR mergeability check passes or fails.
flowchart LR
    Commit[Commit / PR Event] --> ActionsAPI[Actions API]
    ActionsAPI --> DB[(MySQL: Check Suite Table)]
    DB --> Puller[Puller / Orchestrator]
    Puller --> Queue[Execution Job Queue]
    Queue --> Runner[Runners / Executors]

Because GitHub operates at planetary scale with millions of daily commits and continuous automations, the generation of check_suite and check_run IDs is an extremely high-frequency event. Tables handling these entities experience explosive sequential ID growth.


2. The Integer Limit & The 6-Month Advance Migration

The 32-Bit Ceiling

By default, standard relational database INT types are 32-bit signed integers:

  • Signed 32-bit integer range: 231-2^{31} to 23112^{31} - 1 (2,147,483,6472,147,483,647, or roughly 2.142.14 billion).
  • Unsigned 32-bit integer range: 00 to 23212^{32} - 1 (4,294,967,2954,294,967,295, or roughly 4.294.29 billion).

When an auto-incrementing primary key reaches 2,147,483,6472,147,483,647 on a signed column, subsequent INSERT operations fail with duplicate key or out-of-range exceptions.

GitHub’s Proactive Fix

GitHub was fully aware of this trajectory. Having dealt with auto-increment ID exhaustions in other parts of their infrastructure earlier in their history, they had monitoring alerts that tracked the auto-increment headroom.

Six months prior to March 2021, GitHub proactively initiated and completed a large-scale database schema migration:

  • Target columns (check_suite_id, check_run_id) were altered from INT (32-bit) to BIGINT (64-bit).
  • A signed 64-bit integer extends the maximum value up to 9,223,372,036,854,775,8079,223,372,036,854,775,807 (9.22×10189.22 \times 10^{18}), making ID exhaustion practically impossible under any realistic timeframe.
  • GitHub’s primary internal services were written in languages like Ruby, Go, and C#, none of which enforced restrictive manual downcasting to 32-bit signed integers for these identifiers.

From GitHub’s internal perspective, the system was completely insulated from 32-bit overflow.


3. What Went Wrong: The Latent Dependency Blind Spot

On March 1, 2021, the check_suite auto-increment counter naturally crossed the 2,147,483,6472,147,483,647 mark (23112^{31} - 1). The database accepted the writes without friction, successfully generating IDs like 2,147,483,6482,147,483,648.

Immediately, the Actions service began throwing high error rates, queue processing stalled, and workflows sat perpetually in a pending state.

Root Cause Breakdown

sequenceDiagram
    autonumber
    participant DB as MySQL (BIGINT)
    participant API as Puller / Ingestion Service
    participant Lib as Third-Party GraphQL Lib
    participant Queue as Execution Queue

    API->>DB: INSERT / Read Check Suite (ID > 2^31 - 1)
    DB-->>API: Returns Check Suite record (int64)
    API->>Lib: Unmarshal / Process JSON payload
    Note over Lib: Library assumes int32 for ID
    Lib-->>API: JSON Unmarshaling Exception / Overflow Error!
    API-xQueue: Failed to enqueue tasks
    Note over Queue: Check suites stay stuck in 'Pending'
  1. Successful Database Insertion: The database engine was operating on BIGINT. Check suites with IDs >2311> 2^{31} - 1 were persisted without issue.
  2. The Intermediate Puller: A background orchestration component pulled records out of the database or received API responses, serializing/deserializing data to distribute work to the execution queues.
  3. The Hidden Parser: Within this orchestration path sat a third-party GraphQL library responsible for unmarshaling JSON payloads.
  4. The Hardcoded 32-bit Schema: The library’s internal schema parsing logic unmarshaled IDs into standard 32-bit signed integers (int32) rather than arbitrary-precision numbers or 64-bit integers (int64).
  5. Unmarshaling Failure: As soon as the ID crossed 23112^{31} - 1, the GraphQL library threw an unmarshaling error on every single workflow run.
  6. Resulting Cascade: Because unmarshaling aborted the pipeline, jobs were never enqueued. The database entries existed, but downstream runners were starved for tasks, leaving check suites indefinitely pending.

4. Secondary Impacts: Search and Indexing Degradation

The outage was not confined strictly to job execution; it cascaded into peripheral services:

  • Incomplete Search Indices: GitHub uses search clusters (e.g., Elasticsearch) to index repositories, commits, and workflow run metadata.
  • Indexer Pipeline Shared the Dependency: The indexing worker responsible for syncing workflow metadata into the search cluster relied on the exact same GraphQL parsing layer.
  • Data Dropped from Search: Because unmarshaling crashed the ingest pipeline, newly created workflows with IDs >2311> 2^{31} - 1 were never pushed into Elasticsearch. As a result, users querying their actions or workflow histories observed stale, missing, or broken search results.

5. Mitigation and Resolution

Once the on-call engineers isolated the failures to JSON unmarshaling inside the GraphQL dependency:

  1. Code Patch Deployment: GitHub patched the offending layer to properly treat check suite and check run IDs as 64-bit integers during serialization and deserialization.
  2. Queue Draining: After the parser fix was deployed, the puller services resumed normal operation, successfully processed the backlog of stalled check suites, and forwarded jobs to the waiting execution runners.
  3. Backfill Indexing: Historical events that had failed to index were reprocessed to restore search consistency.

6. Architectural Lessons and Takeaways

1. The Reality of Systems Blind Spots

Systems at scale cannot be fully modeled in an engineer’s head. You can audit your internal source code for explicit type casts, run database schema migrations, and verify your core programming languages (Go, Ruby, C#), but third-party libraries, serializers, and RPC frameworks often make subtle, implicit assumptions.

2. The Danger of Transitive Dependencies

Developers routinely pull dependencies via package managers (npm, pip, cargo, gem) without auditing how these libraries serialize primitive types under the hood. A protocol or data layer library that assumes standard JSON numbers fit into signed 32-bit registers is an architectural landmine for high-throughput distributed systems.

3. Auditing Classes of Failures

Following the incident, GitHub did not just patch the single GraphQL library; they conducted an infrastructure-wide audit of all external dependencies across their technology stack. The goal was to eliminate the entire class of failure (32-bit integer limits during serialization) across every API, messaging schema, and storage abstraction.

Summary Checklist for Primary Key Scale

LayerVerification Item
Storage EngineEnsure primary and foreign keys use BIGINT (64-bit).
Application RuntimeConfirm native language types avoid lossy casting down to int32.
Network & SerializationVerify JSON, Protobuf, and GraphQL parsers unmarshal IDs as strings or 64-bit integers.
Client LibrariesValidate that client SDKs consuming public APIs do not break when receiving 64-bit integer responses.
Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses