CrewAI Tutorial (TypeScript): rate limiting API calls for beginners

By Cyprian AaronsUpdated 2026-04-21
crewairate-limiting-api-calls-for-beginnerstypescript

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
  • crewai installed in your project
  • dotenv installed 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

  1. 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();
  }
}
  1. 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;
  });
}
  1. Load secrets from .env and 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],
});
  1. 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);
});
  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 429 responses.
  • 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

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

Related Guides