From bamboohr-pack
Implements BambooHR API rate limit handling with 503/Retry-After backoff, exponential retries with jitter, and request optimization for 429/503 errors.
How this skill is triggered — by the user, by Claude, or both
Slash command
/bamboohr-pack:bamboohr-rate-limitsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
BambooHR does not publish exact rate limits, but the API returns `503 Service Unavailable` with a `Retry-After` header when you exceed them. This skill covers detection, backoff, request optimization, and queue-based throttling.
BambooHR does not publish exact rate limits, but the API returns 503 Service Unavailable with a Retry-After header when you exceed them. This skill covers detection, backoff, request optimization, and queue-based throttling.
BambooHR rate limiting details:
| Signal | Value | Description |
|---|---|---|
| HTTP Status | 503 | Primary rate limit signal |
Retry-After header | seconds (e.g., 30) | How long to wait before retrying |
X-BambooHR-Error-Message | varies | May contain rate limit detail |
HTTP Status 429 | rare | Some endpoints return 429 for employee count limits |
Key insight: BambooHR uses 503 (not 429) for rate limiting. Failed authentication attempts also count toward rate limits, so ensure your API key is valid before making many requests.
import { BambooHRApiError } from './client';
interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
}
const DEFAULT_RETRY: RetryConfig = {
maxRetries: 5,
baseDelayMs: 1000,
maxDelayMs: 60_000,
};
async function withBambooHRRetry<T>(
operation: () => Promise<T>,
config = DEFAULT_RETRY,
): Promise<T> {
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await operation();
} catch (err) {
if (attempt === config.maxRetries) throw err;
if (!(err instanceof BambooHRApiError)) throw err;
if (!err.retryable) throw err; // Only retry 429, 503, 500, 502
// Honor BambooHR's Retry-After header
let delay: number;
if (err.meta.retryAfter) {
delay = parseInt(err.meta.retryAfter, 10) * 1000;
} else {
// Exponential backoff with jitter
const exponential = config.baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * config.baseDelayMs;
delay = Math.min(exponential + jitter, config.maxDelayMs);
}
console.warn(
`BambooHR rate limited (attempt ${attempt + 1}/${config.maxRetries}). ` +
`Waiting ${(delay / 1000).toFixed(1)}s...`
);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('unreachable');
}
import PQueue from 'p-queue';
// BambooHR unofficial guidance: stay under ~10 requests/second
const bamboohrQueue = new PQueue({
concurrency: 3, // Max 3 concurrent requests
interval: 1000, // Per 1-second window
intervalCap: 8, // Max 8 requests per second
});
async function rateLimitedRequest<T>(operation: () => Promise<T>): Promise<T> {
return bamboohrQueue.add(() => withBambooHRRetry(operation));
}
// Usage — all requests go through the queue
const employees = await rateLimitedRequest(() => client.getDirectory());
const report = await rateLimitedRequest(() => client.customReport(['firstName', 'lastName']));
// Bulk operations automatically throttled
const employeeDetails = await Promise.all(
employeeIds.map(id =>
rateLimitedRequest(() => client.getEmployee(id, ['firstName', 'lastName', 'jobTitle']))
),
);
Use custom reports instead of individual GETs:
// BAD: N+1 requests (one per employee)
const employees = await client.getDirectory();
for (const emp of employees.employees) {
const detail = await client.getEmployee(emp.id, ['salary', 'department']);
// 500 employees = 501 requests
}
// GOOD: 1 request using custom report
const report = await client.customReport([
'firstName', 'lastName', 'department', 'jobTitle', 'hireDate',
]);
// 1 request, all employee data
Use incremental sync:
// BAD: Full directory pull every time
const allEmployees = await client.getDirectory();
// GOOD: Only changed employees since last sync
const changed = await client.request<any>(
'GET', `/employees/changed/?since=${lastSyncTimestamp}`,
);
// Only fetch details for employees that actually changed
Use table changed endpoint:
// GET /employees/changed/tables/{tableName}?since=...
const changedJobs = await client.request<any>(
'GET', `/employees/changed/tables/jobInfo?since=${lastSyncTimestamp}`,
);
class BambooHRRateLimitMonitor {
private requestLog: { timestamp: number; status: number }[] = [];
private rateLimitHits = 0;
recordRequest(status: number) {
this.requestLog.push({ timestamp: Date.now(), status });
// Only keep last 5 minutes
const cutoff = Date.now() - 5 * 60 * 1000;
this.requestLog = this.requestLog.filter(r => r.timestamp > cutoff);
if (status === 503 || status === 429) {
this.rateLimitHits++;
}
}
getStats() {
const recent = this.requestLog;
return {
requestsLast5Min: recent.length,
requestsPerSecond: (recent.length / 300).toFixed(2),
rateLimitHits: this.rateLimitHits,
errorRate: recent.filter(r => r.status >= 400).length / Math.max(recent.length, 1),
};
}
shouldBackOff(): boolean {
const stats = this.getStats();
return stats.errorRate > 0.1 || parseFloat(stats.requestsPerSecond) > 8;
}
}
Retry-After header| Signal | Detection | Action |
|---|---|---|
503 + Retry-After: N | Check response status + header | Wait N seconds, then retry |
503 without Retry-After | Status only | Exponential backoff from 1s |
429 (employee limit) | Status code | Contact BambooHR to increase limit |
| Many consecutive 503s | Monitor hit count | Pause all requests for 60s |
bamboohr-webhooks-events)For security configuration, see bamboohr-security-basics.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin bamboohr-packAudits BambooHR API usage, reduces calls via caching and patterns, and monitors to prevent rate limits in integrations.
Handles RemoFirst API rate limits with Python client patterns, 429 error backoff, and integration guidance for global HR, EOR, payroll workflows.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.