GitHub Async Merge API: Merge Stacked Pull Requests Without Timeouts

Use GitHub's asynchronous merge API to merge pull requests in the background, support stacked PRs, and handle merge queues with polling instead of fragile long-running requests.

Layered pull request cards joining a single trunk line, showing a dependent change moving through an asynchronous merge process

GitHub's async merge API is now generally available. It gives automation a better way to merge pull requests when the operation may involve a merge queue, a complex merge, or a stack of dependent PRs. The official announcement recommends this path for programmatic merges instead of the older synchronous REST endpoint or GraphQL mutations.

The change matters if you have a bot that merges approved pull requests, promotes a release branch, or coordinates stacked pull requests. A merge request is accepted, processed in the background, and checked through a separate status endpoint. Your worker can submit the operation, release its request slot, and continue polling without holding an HTTP request open while GitHub evaluates the merge.

Why the async endpoint is different

The endpoint is `PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge-async`. A new operation returns HTTP 202 and a UUID. You then call `GET /repos/{owner}/{repo}/pulls/{pull_number}/merge-async/{uuid}` to read the current result. GitHub documents four useful states: `pending`, `merged`, `enqueued`, and `failed`.

  • `pending` means the background merge is still running.
  • `merged` means GitHub completed the merge and returns the merge commit OID.
  • `enqueued` means the pull request entered a merge queue. It does not mean the merge has completed.
  • `failed` means the operation stopped and includes a message describing the failure.

Stacked pull requests are a first-class use case

The async API is required for merging a stacked pull request. When you request a merge for one entry, GitHub includes the open downstack pull requests in the operation. That lets a bot submit the stack as one operation instead of trying to merge each branch with a separate synchronous request. The REST API documentation also spells out that the result `enqueued` is final for the merge-queue request, but you must check the pull request later to learn whether the queue eventually merged it.

Submit a merge request with a stable head SHA

A safe automation records the pull request head SHA before submitting the merge. Pass that SHA as `sha` so GitHub can cancel the operation if the branch changes between submission and execution. For a direct merge, choose `merge`, `squash`, or `rebase`. Set `merge_action` to `direct_merge`, `merge_queue`, or `default`; `default` uses the queue when the target branch has one configured.

curl -L -X PUT -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GITHUB_TOKEN" -H "X-GitHub-Api-Version: 2026-03-10" https://api.github.com/repos/OWNER/REPO/pulls/42/merge-async -d '{"sha":"EXPECTED_HEAD_SHA","merge_method":"squash","merge_action":"default"}'

The token needs the repository Contents permission with write access. Keep it in your secret manager, not in workflow output or a repository file. If another async merge is already pending for the pull request, GitHub returns 409 with the existing request UUID and merge options. Treat that as a signal to resume polling instead of submitting a second operation.

Poll with backoff and handle queue semantics

Poll the result endpoint with a short initial delay, then increase the delay between requests. Stop on `merged` or `failed`. Stop on `enqueued` too, but schedule a separate check for the pull request's merged state because the queue result only confirms that GitHub accepted the pull request into the queue. The request result is retained for 24 hours after its most recent update, so do not treat a later 404 as proof that the merge failed.

curl -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GITHUB_TOKEN" -H "X-GitHub-Api-Version: 2026-03-10" https://api.github.com/repos/OWNER/REPO/pulls/42/merge-async/REQUEST_UUID

Do not bypass repository rules by accident

The endpoint accepts `bypass_rules`, but it defaults to false. Only set it when the authenticated actor is allowed to bypass the relevant repository rules and the automation has a documented reason. A successful 202 only means GitHub accepted the background operation. It is not an approval to skip required checks, reviews, or deployment gates.

Plan for the response codes

A 202 is the normal accepted path. A 200 can mean the pull request was already merged or is already in a merge queue. A 400 means the basic pull request state is not ready, such as a closed or draft PR. A 409 means there is already an async merge request for the pull request. A 403 points to permissions, while 422 usually means validation failed or the API was rate limited for the operation. Log the status, pull request number, request UUID, and a redacted failure message so an operator can resume the right operation.

A small migration checklist

  1. Find automation using the synchronous merge REST endpoint or GraphQL merge mutations.
  2. Add a request record with the pull request number, expected head SHA, merge action, and polling deadline.
  3. Submit to `/merge-async` and persist the returned UUID before polling.
  4. Treat `enqueued` as queue acceptance and confirm the eventual merge separately.
  5. Test direct merges, merge queues, stacked pull requests, duplicate submissions, and a changed head SHA.

The async merge API is a small protocol change with a useful operational effect: submit, store the request ID, and observe the result instead of guessing how long a merge will take. For new merge automation, start here. For an existing bot, migrate one repository first and make the queue and stacked-PR states visible in its logs before expanding the rollout.