How to Manage GitHub API Rate Limiting and Handle Errors in Spec-Kit
Spec-Kit handles GitHub API rate limiting by parsing standard HTTP headers to extract limit details and reset times, then raises descriptive RuntimeError exceptions that guide users toward authentication tokens when quotas are exhausted.
The Spec-Kit CLI interacts with the GitHub REST API to download release assets for AI assistant templates, making robust GitHub API rate limiting and error handling essential for reliable operation. When unauthenticated requests hit the 60-requests-per-hour ceiling or authenticated users approach the 5,000-requests-per-hour limit, the tool degrades gracefully with actionable feedback rather than opaque HTTP errors. The implementation in src/specify_cli/__init__.py provides a transparent strategy for detecting throttling and guiding remediation.
Parsing GitHub API Rate Limit Headers
The foundation of Spec-Kit's rate limit management lies in _parse_rate_limit_headers, a private helper defined at lines 69‑86 in src/specify_cli/__init__.py. This function extracts the standard X-RateLimit-* headers—Limit, Remaining, and Reset—along with the optional Retry-After header.
The parser converts the epoch timestamp from X-RateLimit-Reset into a UTC datetime object and derives a local-time version for easier user display. It also handles integer conversion for Retry-After values when present, returning a structured dictionary that downstream functions consume for error formatting.
def _parse_rate_limit_headers(headers: httpx.Headers) -> dict:
info = {}
if "X-RateLimit-Limit" in headers:
info["limit"] = headers.get("X-RateLimit-Limit")
if "X-RateLimit-Remaining" in headers:
info["remaining"] = headers.get("X-RateLimit-Remaining")
if "X-RateLimit-Reset" in headers:
reset_epoch = int(headers.get("X-RateLimit-Reset", "0"))
if reset_epoch:
reset = datetime.fromtimestamp(reset_epoch, tz=timezone.utc)
info["reset_epoch"] = reset_epoch
info["reset_time"] = reset
info["reset_local"] = reset.astimezone()
if "Retry-After" in headers:
try:
info["retry_after_seconds"] = int(headers.get("Retry-After"))
except ValueError:
info["retry_after"] = headers.get("Retry-After")
return info
Formatting User-Friendly Error Messages
Once headers are parsed, _format_rate_limit_error (lines 97‑124 in src/specify_cli/__init__.py) assembles a multi-line message that surfaces critical details to developers. The function receives the HTTP status code, raw headers, and the URL that triggered the failure, then calls the parser to extract structured data.
The formatted output includes the rate limit ceiling, remaining requests, exact reset time in local timezone, and retry-after duration when available. It concludes with troubleshooting tips that explicitly recommend using a personal access token via --github-token, GH_TOKEN, or GITHUB_TOKEN environment variables to upgrade from the 60-requests-per-hour anonymous quota to the 5,000-requests-per-hour authenticated tier.
def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) -> str:
rate_info = _parse_rate_limit_headers(headers)
lines = [f"GitHub API returned status {status_code} for {url}", ""]
if rate_info:
lines.append("[bold]Rate Limit Information:[/bold]")
if "limit" in rate_info:
lines.append(f" • Rate Limit: {rate_info['limit']} requests/hour")
if "remaining" in rate_info:
lines.append(f" • Remaining: {rate_info['remaining']}")
if "reset_local" in rate_info:
lines.append(
f" • Resets at: {rate_info['reset_local'].strftime('%Y-%m-%d %H:%M:%S %Z')}"
)
if "retry_after_seconds" in rate_info:
lines.append(f" • Retry after: {rate_info['retry_after_seconds']} seconds")
lines.append("")
lines.extend(
[
"[bold]Troubleshooting Tips:[/bold]",
" • If you're on a shared CI or corporate environment, you may be rate‑limited.",
" • Consider using a GitHub token via --github-token or the GH_TOKEN/GITHUB_TOKEN environment variable to increase rate limits.",
" • Authenticated requests have a limit of 5,000 /hour vs 60 /hour for unauthenticated.",
]
)
return "\n".join(lines)
Implementing Rate Limit Handling in Download Workflows
The download_template_from_github function (lines 721‑724 and 771‑774) applies this error handling logic during both metadata retrieval and asset streaming. When fetching release information from https://api.github.com/repos/github/spec-kit/releases/latest, the CLI checks for a 200 OK response. Any other status triggers a RuntimeError containing the formatted rate limit message.
The same validation occurs when initiating the download stream for the actual template asset, ensuring consistent GitHub API error handling across both the lightweight JSON API call and the potentially large binary transfer. Top-level command handlers catch these exceptions, render them inside a Rich Panel for visual clarity, and exit with typer.Exit(1) to signal failure to shell environments and CI pipelines.
def download_template_from_github(ai_assistant: str, download_dir: Path, *,
script_type: str = "sh", verbose: bool = True,
client: httpx.Client = None,
github_token: str = None) -> Tuple[Path, dict]:
api_url = "https://api.github.com/repos/github/spec-kit/releases/latest"
response = client.get(api_url,
headers=_github_auth_headers(github_token),
timeout=30,
follow_redirects=True)
if response.status_code != 200:
# Rate‑limit aware error handling
raise RuntimeError(_format_rate_limit_error(response.status_code,
response.headers,
api_url))
# ... locate matching asset ...
with client.stream("GET", download_url,
headers=_github_auth_headers(github_token)) as dl:
if dl.status_code != 200:
raise RuntimeError(_format_rate_limit_error(dl.status_code,
dl.headers,
download_url))
# continue streaming download...
When the anonymous quota is exhausted, users see structured output like this:
GitHub API returned status 403 for https://api.github.com/repos/github/spec-kit/releases/latest
Rate Limit Information:
• Rate Limit: 60 requests/hour
• Remaining: 0
• Resets at: 2026-03-06 03:15:42 UTC
• Retry after: 3600 seconds
Troubleshooting Tips:
• If you're on a shared CI or corporate environment, you may be rate-limited.
• Consider using a GitHub token via --github-token or the GH_TOKEN/GITHUB_TOKEN environment variable to increase rate limits.
• Authenticated requests have a limit of 5,000/hour vs 60/hour for unauthenticated.
Summary
- Header Inspection: The
_parse_rate_limit_headersfunction insrc/specify_cli/__init__.pyextractsX-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset, andRetry-Afterheaders to determine exactly when quotas reset. - Actionable Errors:
_format_rate_limit_errorconverts raw HTTP data into human-readable messages that specify reset times and authentication remedies. - Workflow Integration: The
download_template_from_githubfunction applies these helpers to both API metadata requests and asset download streams, raisingRuntimeErrorexceptions that bubble up to Rich-rendered CLI output. - Authentication Guidance: The error messages explicitly guide users to supply tokens via
--github-token,GH_TOKEN, orGITHUB_TOKENto upgrade from 60 requests/hour to 5,000 requests/hour.
Frequently Asked Questions
What are the GitHub API rate limits when using Spec-Kit?
Unauthenticated requests are limited to 60 requests per hour, while authenticated requests using a personal access token allow 5,000 requests per hour. The Spec-Kit source code references these limits in error messages to encourage token usage when the anonymous quota is exhausted.
How does Spec-Kit detect when rate limiting occurs?
The tool inspects HTTP response headers via _parse_rate_limit_headers in src/specify_cli/__init__.py (lines 69‑86). It looks for X-RateLimit-Remaining reaching zero or receiving a 403 Forbidden status, then parses the X-RateLimit-Reset epoch timestamp to calculate when the quota refreshes.
What should I do when I see a rate limit error in Spec-Kit?
Supply a GitHub personal access token using the --github-token CLI option or by setting the GH_TOKEN or GITHUB_TOKEN environment variable. The error output explicitly states when your current quota resets and reminds you that authenticated access provides significantly higher limits than anonymous usage.
Where is the rate limiting logic implemented in the Spec-Kit codebase?
The core logic resides in src/specify_cli/__init__.py. The helper functions _parse_rate_limit_headers and _format_rate_limit_error live at lines 69‑86 and 97‑124 respectively, while the download_template_from_github function (lines 721‑724 and 771‑774) consumes these helpers during release asset retrieval.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →