CrewAI Tutorial (TypeScript): rate limiting API calls for beginners
This tutorial shows you how to rate limit API calls inside a CrewAI TypeScript workflow so your agents stop hammering external services and triggering 429s. You need this when your crew calls OpenAI, Anthropic, Stripe, a CRM, or any API with per-minute quotas.
What You'll Need
- •Node.js 18+
- •TypeScript 5+
- •A CrewAI TypeScript project already set up
- •
crewaiinstalled in your project - •
dotenvinstalled for environment variables - •An API key for the service you want to call
- •A basic understanding of async/await and Promises
Install the packages if you haven’t already:
npm install crewai dotenv
npm install -D typescript tsx @types/node
Step-by-Step
- •Create a small rate limiter first. Do not hide this inside the agent logic. Keep it as a reusable utility so every task that touches external APIs uses the same throttling rules.
export class RateLimiter {
private queue: Array<() => void> = [];
private running = 0;
constructor(
private readonly maxConcurrent: number,
private readonly minTimeMs: number
) {}
async schedule<T>(fn: () => Promise<T>): Promise<T> {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
private acquire(): Promise<void> {
return new Promise((resolve) => {
const tryRun = () => {
if (this.running < this.maxConcurrent) {
this.running++;
setTimeout(resolve, this.minTimeMs);
} else {
this.queue.push(tryRun);
}
};
tryRun();
});
}
private release() {
this.running--;
const next = this.queue.shift();
if (next) next();
}
}
- •Put your API client behind that limiter. This example uses
fetch, which is available in Node 18+, and wraps every request so your crew cannot exceed the configured pace.
import { RateLimiter } from "./rate-limiter";
const limiter = new RateLimiter(2, 1000);
export async function getJson<T>(url: string, apiKey: string): Promise<T> {
return limiter.schedule(async () => {
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error(`API request failed: ${res.status} ${res.statusText}`);
}
return (await res.json()) as T;
});
}
- •Load secrets from
.envand create a CrewAI agent that uses a tool calling your rate-limited client. The important part is that the agent never talks to the external API directly; it only goes through the tool.
import "dotenv/config";
import { Agent, Task, Crew } from "crewai";
import { getJson } from "./api-client";
const apiKey = process.env.EXTERNAL_API_KEY;
if (!apiKey) throw new Error("Missing EXTERNAL_API_KEY");
const fetchCustomerTool = async (customerId: string) => {
return getJson<{ id: string; name: string }>(
`https://api.example.com/customers/${customerId}`,
apiKey
);
};
const agent = new Agent({
role: "Support analyst",
goal: "Fetch customer data safely without exceeding API limits",
backstory: "You work with external systems that enforce strict quotas.",
});
const task = new Task({
description: "Get customer profile for customer_id=1234",
agent,
});
const crew = new Crew({
agents: [agent],
tasks: [task],
});
- •Wire the tool into execution and make multiple calls to prove the limiter works. This is where beginners usually fail: they test one request and assume they are safe. You want to test bursts, not single calls.
async function main() {
const requests = ["1234", "5678", "9012", "3456"];
const results = await Promise.all(
requests.map(async (id) => {
const customer = await fetchCustomerTool(id);
console.log("Fetched:", customer.name);
return customer;
})
);
console.log(`Completed ${results.length} requests`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
- •If you want stricter control across the whole crew, wrap every tool in the same limiter instance. That gives you one global throttle instead of per-tool throttles, which is what you want when all tools hit the same vendor quota.
import { RateLimiter } from "./rate-limiter";
const sharedLimiter = new RateLimiter(1, 1200);
export async function limitedToolCall<T>(fn: () => Promise<T>): Promise<T> {
return sharedLimiter.schedule(fn);
}
async function exampleUsage() {
const result = await limitedToolCall(async () => {
const res = await fetch("https://api.example.com/status");
if (!res.ok) throw new Error("Status check failed");
return res.text();
});
console.log(result);
}
Testing It
Run your script with several parallel requests and watch the spacing between outbound calls. If you hit an API with rate limits, check that you no longer see bursts of 429 Too Many Requests. Add logging around schedule() so you can confirm only the allowed number of concurrent requests runs at once.
A good test is to temporarily lower maxConcurrent to 1 and minTimeMs to 2000, then fire five requests at once. You should see them complete one by one instead of all at once.
Next Steps
- •Add exponential backoff for retries after
429responses. - •Move from a simple in-memory limiter to Redis if you run multiple worker processes.
- •Wrap CrewAI tools with observability so you can trace which agent caused each API call burst.
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