cli / cli/cli

gh issue list fails with stale rate-limit headers from cached introspection response

Open
#12,812 5 comments 2 reactions 0 assignees View on GitHub
bug needs-investigation priority-3 stale
Dominant language
Go
Stars
46.3k
Forks
9k
Avg merge
2d 9h
Merged PRs (30d)
89

Description

### Describe the bug

After a GraphQL rate limit is exhausted and then resets, `gh issue list` (and other subcommands using GraphQL) continues to fail with "API rate limit exceeded" for up to 24 hours. Meanwhile, `gh api graphql` works fine with the same token.

The root cause: `gh` caches a `SearchType` introspection query with `X-Gh-Cache-Ttl: 24h`. When this query is made during rate limit exhaustion, the cached **response** carries `X-Ratelimit-Remaining: 0` in its headers. After the actual rate limit resets (~1 hour), `gh` serves this response from cache and reads the stale rate-limit headers, refusing to make new requests.

### Steps to reproduce the behavior

1. Exhaust the GraphQL rate limit (e.g. via automated tooling making many `gh issue list` calls)
2. Wait for the rate limit to reset (~1 hour)
3. Verify the rate limit has reset:
```
$ gh api rate_limit --jq '.resources.graphql'
{"limit":5000,"remaining":4148,"reset":1772334629,"resource":"graphql","used":852}
```
4. Try `gh issue list`:
```
$ gh issue list --repo owner/repo --limit 1 --json number
GraphQL: API rate limit already exceeded for user ID 477956.
```
5. Meanwhile, direct GraphQL works fine:
```
$ gh api graphql -f query='{ viewer { login } rateLimit { remaining } }'
{"data":{"viewer":{"login":"kim-em"},"rateLimit":{"remaining":4136}}}
```

### Evidence from `GH_DEBUG=api`

```
* Request at 2026-03-01 12:15:24 +0000 UTC
* Request to https://api.github.com/graphql
> X-Gh-Cache-Ttl: 24h0m0s <-- request asks for 24h cache

< HTTP/2.0 200 OK
< Date: Sun, 01 Mar 2026 01:42:54 GMT <-- response is ~10h old (from cache)
< X-Ratelimit-Remaining: 0 <-- stale! actual remaining is 4148
< X-Ratelimit-Reset: 1772331384
< X-Ratelimit-Used: 5001
```

The response `Date` header (`01:42:54`) is from when the rate limit was exhausted. The request timestamp (`12:15:24`) is current. `gh` is serving the cached response and honoring its stale rate-limit headers.

### Root cause

The `SearchType` introspection query (used internally by `gh issue list` and similar subcommands) is cached with a 24-hour TTL. The rate-limit headers are properties of the request moment, not the response content. When gh serves a cached response, it should not use the cached rate-limit headers to gate new requests.

### Workaround

Delete the cached introspection response:

```sh
# Find and remove cached responses with exhausted rate limits
grep -rl "X-Ratelimit-Remaining: 0" ~/.cache/gh/ 2>/dev/null | xargs rm -f
```

### Expected vs actual behavior

**Expected:** After the rate limit resets, `gh issue list` should work again.
**Actual:** `gh issue list` fails for up to 24 hours after rate limit recovery due to stale cached headers.

### Suggested fix

Either:
1. Strip rate-limit headers when storing responses in cache, or
2. Don't check rate-limit headers when serving from cache, or
3. Include rate-limit state in cache invalidation (evict entries whose `X-Ratelimit-Reset` timestamp has passed)

### gh version

```
gh version 2.83.2 (nixpkgs)
```

### Note

This is likely the root cause of many reports in #8321, where users see "rate limit exceeded" while `gh api /rate_limit` shows plenty of remaining quota. That issue was closed without identifying the caching mechanism.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.