Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement Hex rate limiting, backoff, and idempotency patterns. Use when handling rate limit errors, implementing retry logic, or optimizing API request throughput for Hex. Trigger with phrases like "hex rate limit", "hex throttling", "hex 429", "hex retry", "hex backoff".
.claude/skills/jeremylongshore-hex-rate-limits/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -24% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 0% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 23% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 20% | 0% |
Hex's API enforces tight limits on project run triggers (20 per minute, 60 per hour) while leaving read operations like status checks and project listing largely unthrottled. Data teams scheduling batch analytics runs or triggering parameterized notebooks from CI/CD pipelines must carefully manage the hourly cap, since a single pipeline triggering 15 projects can consume a quarter of the hourly budget. Polling run status is free, but triggering runs is the bottleneck that shapes integration architecture.
| Endpoint | Limit | Window | Scope | |----------|-------|--------|-------| | RunProject (trigger) | 20 req | 1 minute | Per API token | | RunProject (trigger) | 60 req | 1 hour | Per API token | | GetRunStatus | No hard limit | - | Per API token | | ListProjects | No hard limit | - | Per API token | | CancelRun | No hard limit | - | Per API token |
typescriptclass HexRateLimiter { private minuteTokens: number = 20; private hourlyTokens: number = 60; private lastMinuteRefill: number = Date.now(); private lastHourlyRefill: number = Date.now(); private queue: Array<{ resolve: () => void }> = []; async acquire(): Promise<void> { this.refill(); if (this.minuteTokens >= 1 && this.hourlyTokens >= 1) { this.minuteTokens -= 1; this.hourlyTokens -= 1; return; } return new Promise(resolve => this.queue.push({ resolve })); } private refill() { const now = Date.now(); this.minuteTokens = Math.min(20, this.minuteTokens + ((now - this.lastMinuteRefill) / 60_000) * 20); this.lastMinuteRefill = now; this.hourlyTokens = Math.min(60, this.hourlyTokens + ((now - this.lastHourlyRefill) / 3_600_000) * 60); this.lastHourlyRefill = now; while (this.minuteTokens >= 1 && this.hourlyTokens >= 1 && this.queue.length) { this.minuteTokens -= 1; this.hourlyTokens -= 1; this.queue.shift()!.resolve(); } } } const runLimiter = new HexRateLimiter();
typescriptasync function hexRunWithRetry( projectId: string, params: Record<string, any>, maxRetries = 3 ): Promise<any> { for (let attempt = 0; attempt <= maxRetries; attempt++) { await runLimiter.acquire(); const res = await fetch(`${HEX_BASE}/api/v1/run/${projectId}`, { method: "POST", headers, body: JSON.stringify({ inputParams: params }), }); if (res.ok) return res.json(); if (res.status === 429) { const delay = 30_000 * Math.pow(2, attempt) + Math.random() * 5000; await new Promise(r => setTimeout(r, delay)); continue; } if (res.status >= 500 && attempt < maxRetries) { await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 3000)); continue; } throw new Error(`Hex API ${res.status}: ${await res.text()}`); } throw new Error("Max retries exceeded"); }
typescriptasync function batchRunProjects(projects: Array<{ id: string; params: any }>, batchSize = 5) { const results: any[] = []; for (let i = 0; i < projects.length; i += batchSize) { const batch = projects.slice(i, i + batchSize); const runs = await Promise.all( batch.map(p => hexRunWithRetry(p.id, p.params)) ); // Poll for completion for (const run of runs) { let status = run; while (status.status === "RUNNING") { await new Promise(r => setTimeout(r, 5000)); const res = await fetch(`${HEX_BASE}/api/v1/run/${run.runId}/status`, { headers }); status = await res.json(); } results.push(status); } if (i + batchSize < projects.length) await new Promise(r => setTimeout(r, 15_000)); } return results; }
| Issue | Cause | Fix | |-------|-------|-----| | 429 on RunProject | Exceeded 20/min or 60/hour trigger limit | Queue runs, space 5s apart minimum | | Run stuck in RUNNING | Long-running query or compute timeout | Poll up to 30 min, then CancelRun | | 401 on scheduled run | API token rotated | Refresh token in CI secrets before batch | | Empty run output | Project has no published outputs | Verify project has published cells | | 409 concurrent run | Same project triggered twice | Check run status before re-triggering |
See hex-performance-tuning.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-06 | pass→pass | 14,085 | 14,332 | +2% | 1 | 1 | 0% | 1,513 | 2,600 | +72% | 0 | 0 | — |
case-01 | fail→pass | 43,698 | 28,388 | -35% | 1 | 1 | 0% | 8,276 | 6,302 | -24% | 0 | 0 | — |
case-02 | fail→fail | 25,118 | 25,631 | +2% | 1 | 1 | 0% | 5,275 | 6,806 | +29% | 0 | 0 | — |
case-03 | fail→pass | 25,340 | 8,610 | -66% | 1 | 1 | 0% | 3,057 | 3,062 | +0% | 0 | 0 | — |
case-04 | pass→pass | 18,182 | 18,119 | -0% | 1 | 1 | 0% | 2,927 | 3,891 | +33% | 0 | 0 | — |
case-05 | pass→pass | 17,632 | 22,902 | +30% | 1 | 1 | 0% | 2,114 | 4,343 | +105% | 0 | 0 | — |
case-07 | fail→pass | 8,211 | 6,105 | -26% | 1 | 1 | 0% | 1,335 | 2,524 | +89% | 0 | 0 | — |
case-08 | fail→pass | 13,339 | 12,790 | -4% | 1 | 1 | 0% | 2,101 | 2,585 | +23% | 0 | 0 | — |
case-09 | fail→pass | 17,777 | 12,496 | -30% | 1 | 1 | 0% | 2,162 | 2,597 | +20% | 0 | 0 | — |
case-10 | fail→pass | 15,259 | 8,365 | -45% | 1 | 1 | 0% | 1,721 | 1,939 | +13% | 0 | 0 | — |
case-11 | pass→pass | 16,679 | 11,180 | -33% | 1 | 1 | 0% | 2,201 | 2,635 | +20% | 0 | 0 | — |
case-12 | fail→pass | 11,263 | 2,419 | -79% | 1 | 1 | 0% | 1,183 | 1,697 | +43% | 0 | 0 | — |
case-13 | pass→pass | 10,655 | 9,144 | -14% | 1 | 1 | 0% | 994 | 2,031 | +104% | 0 | 0 | — |
case-14 | fail→pass | 23,424 | 11,209 | -52% | 1 | 1 | 0% | 3,504 | 3,407 | -3% | 0 | 0 | — |
case-15 | fail→pass | 13,853 | 8,206 | -41% | 1 | 1 | 0% | 1,972 | 1,953 | -1% | 0 | 0 | — |
case-16 | pass→pass | 39,241 | 13,372 | -66% | 1 | 1 | 0% | 2,219 | 2,746 | +24% | 0 | 0 | — |
case-17 | fail→fail | 19,581 | 16,800 | -14% | 1 | 1 | 0% | 2,127 | 2,979 | +40% | 0 | 0 | — |
case-18 | fail→fail | 13,639 | 19,173 | +41% | 1 | 1 | 0% | 2,351 | 3,795 | +61% | 0 | 0 | — |
case-19 | fail→pass | 9,781 | 10,704 | +9% | 1 | 1 | 0% | 1,725 | 2,087 | +21% | 0 | 0 | — |
case-20 | pass→pass | 17,973 | 15,268 | -15% | 1 | 1 | 0% | 2,139 | 3,124 | +46% | 0 | 0 | — |
case-21 | pass→pass | 20,746 | 15,583 | -25% | 1 | 1 | 0% | 1,838 | 3,039 | +65% | 0 | 0 | — |
case-22 | pass→pass | 16,315 | 10,001 | -39% | 1 | 1 | 0% | 1,100 | 2,158 | +96% | 0 | 0 | — |
case-23 | fail→fail | 17,617 | 11,844 | -33% | 1 | 1 | 0% | 2,092 | 2,507 | +20% | 0 | 0 | — |
case-24 | fail→pass | 17,947 | 13,833 | -23% | 1 | 1 | 0% | 1,984 | 2,884 | +45% | 0 | 0 | — |
case-25 | fail→pass | 11,103 | 9,157 | -18% | 1 | 1 | 0% | 906 | 2,074 | +129% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +48 percentage points is the difference between those two pass rates over the 25 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.