ghx caches GitHub data locally (by default in a single SQLite database) and serves results from cache without hitting the API after an initial fetch.
The default SQLite backend stores everything in one database:
~/.cache/ghx/cache/cache.db
The legacy file backend stores one JSON file per item:
~/.cache/ghx/cache/<host>/<owner>/<repo>/
├── .cache_info.json
├── issues/
│ ├── 1.json
│ ├── 2.json
│ └── ...
└── prs/
├── 1.json
├── 2.json
└── ...
Override the base directory with --cache-dir:
ghx --cache-dir /tmp/gh-cache cache --repo cli/clighx defaults to an indexed SQLite database and also supports the legacy file backend:
| Backend | Layout | Select |
|---|---|---|
sqlite (default) |
A single SQLite database at <cache-dir>/cache.db with indexed lookups |
--storage sqlite or GHX_STORAGE=sqlite |
file |
One JSON file per issue/PR under <cache-dir>/<host>/<owner>/<repo>/ |
--storage file or GHX_STORAGE=file |
Select the file backend per invocation with --storage, or set GHX_STORAGE in your environment:
ghx --storage file issue list --repo cli/cli --state all
GHX_STORAGE=file ghx pr list --repo cli/cli--storage takes precedence over GHX_STORAGE. Both backends are keyed by the same repository coordinates, so commands and filters behave identically; only the on-disk representation and query performance differ. On large repositories (hundreds to thousands of cached items), listing, filtering, and searching against SQLite is several times faster because it avoids reading and parsing every item file.
The default SQLite backend does not read the file cache. If you have an existing file cache, import it once:
ghx cache migrate # migrate every cached repository
ghx cache migrate --repo cli/cli # migrate a single repositoryThe file cache is left in place (the migration is non-destructive), and re-running it is safe (idempotent). To keep using the file cache without migrating, pass --storage file.
ghx cache
ghx cache --repo cli/cli
ghx cache --cache-duration 120 # treat cache as fresh for 2 hours
ghx cache --cache-duration 0 # always re-fetch (delta)
ghx cache --force # force full re-fetch--type issues or --type prs refreshes only that portion; --type both (the default) refreshes both. Like --since, an explicit --type bypasses the freshness short-circuit:
ghx cache --type issues
ghx cache --type prs
ghx cache --type prs --since 2026-09-01 # combine with a date window
ghx cache --type issues --force # full issue re-fetch onlyNotes:
- A partial run never marks an incomplete cache complete; run a plain
ghx cache(or--force) to finish fetching the missing portion. --forcewith a partial--typeresets only that portion's resume cursor.
--since refreshes only entries created or updated on or after a given date, instead of the default last-cache-write delta:
ghx cache --since 2026-09-01
ghx cache --since 2026-09-01T15:04:05Z
ghx cache --since "2026-09-01 15:04"Accepted formats: YYYY-MM-DD (UTC midnight), RFC3339, and naive datetimes (local time).
Notes:
- Issues use a server-side filter with exact timestamps. PRs are fetched in two phases: a lightweight newest-first walk locates the exact window (so the progress bar knows the total), then the window is fetched in small full-payload pages. Both avoid the search API.
--sincebypasses the freshness short-circuit and cannot be combined with--force.- Using a
--sincedate newer than the last cache write intentionally skips items in between; runghx cache --forceafterwards if you need to backfill them.
First run fetches everything. Example output:
Caching issues for octocat/hello-world...
Cached 42 issue(s).
Caching pull requests for octocat/hello-world...
Cached 15 pull request(s).
Cache updated. Valid for 60 minute(s).
Subsequent runs use a delta fetch, only retrieving items updated since the last cache write:
Fetching issues updated since 2025-01-15 10:30 for octocat/hello-world...
Cached 3 issue(s).
Fetching PRs updated since 2025-01-15 10:30 for octocat/hello-world...
Cached 1 pull request(s).
Cache updated. Valid for 60 minute(s).
The cache metadata (the .cache_info.json file for the file backend, the cache_meta table for SQLite) tracks when the cache was last written and the configured duration. The file-backend form:
{
"cachedAt": "2025-01-15T10:30:00Z",
"duration": 60
}A cache is considered fresh when time.Since(cachedAt) < duration × 1 minute.
When the cache is fresh, list and view commands serve entirely from the cache with no API calls. When stale, they fall back to the GitHub API.
| Command | Behavior |
|---|---|
cache |
Fetches all issues and PRs (all states, with comments). Skips if cache is younger than --cache-duration. Supports delta fetch, --since windowed refresh, and --type issues/prs partial refresh. |
issue list / pr list |
Reads the cache and applies filters when the cache is fresh (indexed SQL predicates for the SQLite backend). Falls back to the GitHub API when stale. Does not write to cache. |
issue view / pr view |
Serves the individual cached item if the full cache is fresh, or if it was written less than 60 minutes ago. Otherwise fetches from the API and saves to cache. --refresh bypasses all checks. |
- Transient failures (rate limits and GitHub 5xx errors such as an HTML 502 from a proxy) are retried automatically with exponential backoff. Each fetched page is already persisted, so if a run still fails, re-running resumes from the last page written.
- Cache data is never automatically cleaned up. Delete
~/.cache/ghx/cache/cache.db(SQLite) or the per-repository directories under~/.cache/ghx/cache/(file) to free space. - The
--mentionand--appfilters cannot be evaluated from cached data and are silently skipped when serving from cache. - Bulk cache operations fetch up to 100 comments per item.