CrewAI Tutorial (TypeScript): rate limiting API calls for advanced developers
This tutorial shows how to put hard rate limits around CrewAI tool calls in TypeScript so your agents stop hammering external APIs and blowing through quotas. You need this when multiple agents share the same vendor limit, when a single task fans out into many tool calls, or when you need deterministic backoff behavior in production.
What You'll Need
- •Node.js 20+
- •A TypeScript project with
ts-nodeor a build step - •
crewai - •
dotenv - •An API key for the external service you want to protect
- •A CrewAI project already set up with at least one agent and one tool
- •Basic familiarity with async/await and promises
Install the packages:
npm install crewai dotenv
npm install -D typescript ts-node @types/node
Step-by-Step
- •Start by creating a shared rate limiter that all tools will use. This example uses a token bucket with a fixed capacity and refill interval, which is predictable and easy to reason about in production.
export class RateLimiter {
private tokens: number;
private lastRefill: number;
constructor(
private readonly capacity: number,
private readonly refillMs: number
) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
async acquire(): Promise<void> {
while (true) {
this.refill();
if (this.tokens > 0) {
this.tokens -= 1;
return;
}
const waitMs = Math.max(25, this.refillMs - (Date.now() - this.lastRefill));
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
private refill(): void {
const now = Date.now();
if (now - this.lastRefill >= this.refillMs) {
this.tokens = this.capacity;
this.lastRefill = now;
}
}
}
- •Wrap your external API call in a function that acquires a token before every request. This keeps the limiter separate from business logic, which matters once you have multiple tools or multiple agents sharing the same upstream quota.
import 'dotenv/config';
import { RateLimiter } from './rate-limiter';
const limiter = new RateLimiter(5, 1000);
export async function fetchWithLimit(url: string): Promise<string> {
await limiter.acquire();
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.EXTERNAL_API_KEY ?? ''}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Upstream failed: ${response.status} ${response.statusText}`);
}
return await response.text();
}
- •Expose that wrapper as a CrewAI tool. In CrewAI TypeScript, tools are plain async functions with metadata attached through the agent configuration, so you can keep the implementation simple and testable.
import { Agent, Task, Crew } from 'crewai';
import { fetchWithLimit } from './api-client';
const rateLimitedTool = async (input: string): Promise<string> => {
const url = `https://api.example.com/search?q=${encodeURIComponent(input)}`;
return await fetchWithLimit(url);
};
const agent = new Agent({
name: 'ResearchAgent',
role: 'Research analyst',
goal: 'Collect facts without exceeding vendor limits',
backstory: 'You are careful with external APIs and always respect quotas.',
tools: [rateLimitedTool],
});
const task = new Task({
description: 'Search for recent policy changes related to claims automation.',
});
const crew = new Crew({
agents: [agent],
tasks: [task],
});
- •Add retry logic only for transient failures, not for quota exhaustion. If you retry everything blindly, you turn a controlled rate limit into a traffic amplifier.
async function withRetry<T>(
fn: () => Promise<T>,
retries = 2,
): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < retries) {
await new Promise((resolve) => setTimeout(resolve, (attempt + 1) * 250));
}
}
}
throw lastError;
}
export async function safeFetch(url: string): Promise<string> {
return await withRetry(async () => {
await limiter.acquire();
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.text();
});
}
- •Run the crew and verify the limiter is actually controlling concurrency. The important part is that all calls go through the same limiter instance; if you create one per tool invocation, you have no protection at all.
async function main() {
const result = await crew.run();
console.log(result);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Testing It
Run several tasks that trigger the same tool in quick succession and watch the timestamps between upstream requests. You should see bursts capped at your configured capacity, then pauses until tokens refill.
Add logging inside acquire() and confirm only five requests pass per second in the example above. If you still see spikes, check whether each agent instance is constructing its own limiter instead of sharing one singleton.
Also test failure paths by forcing a 429 from your upstream API. The tool should fail cleanly without retry storms, and your application logs should show controlled backoff rather than repeated immediate retries.
Next Steps
- •Move from an in-memory limiter to Redis if you run multiple Node processes or workers.
- •Add per-provider limits so one vendor’s quota does not block unrelated tools.
- •Combine rate limiting with circuit breaking so failing APIs stop consuming agent time fast.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit