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.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin bamboohr-packThis skill is limited to using the following tools:
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.
Audits 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.
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.
Share bugs, ideas, or general feedback.
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.