# How Rate Limiting for External API Calls Works in the Macro Repository

> Learn how Macro implements rate limiting for external API calls using Redis, a sliding-window algorithm, and Lua scripts for efficient quota management. Understand cost-based consumption for services like Gmail.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: how-to-guide
- Published: 2026-08-17

---

**Macro implements rate limiting for external API calls using a Redis-backed sliding-window algorithm executed atomically via Lua scripts, with cost-based quota consumption for operations like Gmail and Google Calendar API calls.**

The **macro-inc/macro** repository manages high-volume outbound requests to third-party services such as Gmail and Google Calendar. To prevent quota exhaustion and ensure fair resource distribution across distributed instances, the codebase centralizes rate limiting for external API calls in Redis with atomic enforcement.

## Redis-Backed Sliding-Window Architecture

### The RedisClient Wrapper

The rate limiting infrastructure centers on `RedisClient`, defined in the `redis` crate, which encapsulates configuration for quota management. This wrapper holds `rate_limit_units` (the maximum quota capacity per window) and `rate_limit_secs` (the sliding window duration in seconds). By storing state in Redis rather than in-memory, the system guarantees consistent enforcement across all service instances without race conditions.

### Cost-Based Quota System

Rather than treating all requests equally, Macro employs a **cost-based** approach where each API operation implements a `cost()` method. For example, `GmailApiOperation::cost()` returns the number of quota units a specific operation consumes, allowing fine-grained control where complex operations (like full message fetches) consume more quota than lightweight operations (like listing message IDs).

## The Atomic Lua Script Implementation

The core enforcement logic resides in [`services/email_service/src/util/redis/rate_limit.rs`](https://github.com/macro-inc/macro/blob/main/services/email_service/src/util/redis/rate_limit.rs). Two primary methods handle rate limit checks:

- **`RedisClient::is_rate_limited`** – For Gmail operations, using keys formatted as `gmail-ratelimit:log:<user_id>`
- **`RedisClient::is_calendar_rate_limited`** – For Calendar operations, using keys formatted as `calendar-ratelimit:log:<email_link_id>`

These methods generate unique Redis members formatted as `"<cost>:<uuid>"` and invoke the Lua script via `redis::Script::new`. The script, retrieved through `get_rate_limit_script_with_usage`, performs three atomic operations:

1. **Cleanup** – Removes entries older than the sliding window
2. **Aggregation** – Sums the costs of remaining entries
3. **Enforcement** – Determines if adding the new request cost would exceed the configured limit

The Lua script returns a tuple `[is_limited, unit_count]` where `is_limited == 1` indicates the request should be blocked, eliminating "check-then-set" race windows through atomic execution.

### Back-Fill vs. Interactive Traffic

When the `is_backfill` flag is set to `true`, the system applies `rate_limit_units_backfill`—a lower threshold that ensures background synchronization jobs do not starve normal interactive traffic. This prioritization guarantees that user-facing operations receive quota priority over historical data back-filling.

## Practical Implementation Examples

The following pattern demonstrates checking rate limits before invoking the Gmail API:

```rust
use macro_env_var::env;
use services::email_service::util::redis::{
    RedisClient, RateLimitArgs,
};
use models_email::gmail::operations::GmailApiOperation;

// Initialise a Redis client (configuration comes from Doppler/env vars)
let redis = RedisClient::new(env!("REDIS_URI")).await?;

// Prepare arguments for a Gmail "list messages" operation
let args = RateLimitArgs {
    user_id: user_uuid,
    operation: GmailApiOperation::ListMessages,
    is_backfill: false,
};

// Perform the rate-limit check before issuing the HTTP request
if redis.is_rate_limited(args).await {
    // The user has exhausted their quota; delay or abort the request
    tracing::info!("Rate limited for user {}", user_uuid);
    return Err(YourError::RateLimited);
}

// Otherwise the call is allowed – proceed to invoke the Gmail API
let gmail = GmailApi::new(&redis).await?;
let messages = gmail.list_messages(...).await?;

```

For Google Calendar operations, the implementation uses a fixed cost approach through the dedicated calendar method:

```rust
// Calendar example – uses the same sliding-window script but a fixed cost of 1
let calendar_limited = redis.is_calendar_rate_limited(email_link_id).await;
if calendar_limited {
    tracing::warn!("Calendar rate limit hit for link {}", email_link_id);
    // Handle back-off or retry logic here
}

```

## Key Source Files and Components

- **[`services/email_service/src/util/redis/rate_limit.rs`](https://github.com/macro-inc/macro/blob/main/services/email_service/src/util/redis/rate_limit.rs)** – Implements `is_rate_limited` (Gmail) and `is_calendar_rate_limited` (Calendar) using the Lua script and sliding-window logic.

- **`get_rate_limit_script_with_usage`** (embedded in the above file) – Contains the Lua script that performs atomic cleanup, usage aggregation, and limit enforcement.

- **[`tooling/notification_sandbox/src/adapters/noop_rate_limiter.rs`](https://github.com/macro-inc/macro/blob/main/tooling/notification_sandbox/src/adapters/noop_rate_limiter.rs)** – Provides a stub implementation that always permits requests, useful for local testing without Redis infrastructure.

- **[`services/notification_service/src/main.rs`](https://github.com/macro-inc/macro/blob/main/services/notification_service/src/main.rs)** – Constructs `RateLimitServiceImpl` with `RedisRateLimitAdapter` and injects it into the service stack.

- **[`services/email_service/src/main.rs`](https://github.com/macro-inc/macro/blob/main/services/email_service/src/main.rs)** – Demonstrates instantiation of `RedisProviderRateLimiter` for Gmail API calls.

## Summary

- **Macro uses a Redis-backed sliding-window rate limiter** for all external API calls, ensuring consistent enforcement across distributed service instances.
- **Cost-based quota consumption** allows different API operations to consume varying amounts of quota via the `cost()` method on operation types.
- **Atomic Lua scripts** eliminate race conditions by combining cleanup, aggregation, and enforcement into a single Redis operation.
- **Back-fill prioritization** uses separate `rate_limit_units_backfill` limits to protect interactive traffic from background job saturation.
- **Service-specific implementations** in [`services/email_service/src/util/redis/rate_limit.rs`](https://github.com/macro-inc/macro/blob/main/services/email_service/src/util/redis/rate_limit.rs) handle Gmail and Calendar APIs with distinct Redis key schemas.

## Frequently Asked Questions

### What type of rate limiting algorithm does Macro use?

Macro implements a **sliding-window rate limiting algorithm** backed by Redis. Unlike fixed-window approaches, this tracks the exact timestamp of each request within the configured window duration (`rate_limit_secs`), providing smoother quota distribution and preventing thundering herd issues at window boundaries.

### How does Macro handle different costs for different API operations?

Each API operation type implements a `cost()` method (such as `GmailApiOperation::cost()`) that returns an integer representing quota units consumed. When checking limits, the system formats Redis members as `"<cost>:<uuid>"`, allowing the Lua script to sum actual costs rather than just counting requests. This enables expensive operations (like fetching full message bodies) to consume more quota than cheap operations (like listing message metadata).

### What is the purpose of the back-fill flag in the rate limiter?

The `is_backfill` boolean flag indicates whether the request originates from a background synchronization job rather than user-initiated traffic. When `true`, the rate limiter applies `rate_limit_units_backfill`—a lower threshold than the standard `rate_limit_units`—ensuring that historical data imports do not exhaust quotas needed for real-time user interactions.

### Why does Macro use Lua scripts for rate limiting?

Macro uses Lua scripts via `redis::Script::new` to guarantee **atomicity** during the check-and-set operation. Without Lua, separate Redis commands for cleanup, sum calculation, and limit enforcement would create race conditions where concurrent requests could both pass the check before either updates the counter. The Lua script executes entirely on the Redis server, reducing round-trips and ensuring consistent state even under high concurrency.