Retry Mechanisms and Rate Limiting Strategies in ViMax: A Complete Technical Guide

ViMax implements a dual-layer resilience system combining Tenacity-based decorators for automatic retries with a custom RateLimiter class for per-minute and per-day API quota management, ensuring robust handling of transient failures and external service limits.

The HKUDS/ViMax repository orchestrates complex video generation workflows that depend on multiple external APIs, making resilient error handling essential for production stability. Understanding the retry mechanisms and rate limiting strategies implemented in ViMax helps developers build agents that gracefully recover from network interruptions and respect external service quotas.

Retry Mechanisms in ViMax

ViMax employs two distinct retry strategies: declarative decorators for general error recovery and manual exponential backoff loops for specific HTTP 429 responses.

Tenacity-Based Decorators

The utils/retry.py file centralizes retry logging logic through a reusable after callback that captures exception details and attempt counts:


# utils/retry.py

def after_func(retry_state: tenacity.RetryCallState) -> None:
    if retry_state.outcome.failed:
        exc = retry_state.outcome.exception()
        logging.warning(
            f"Retrying {retry_state.fn.__name__} due to {repr(exc)} "
            f"(Attempt {retry_state.attempt_number})"
        )
        logging.debug(traceback.format_exception(type(exc), exc, exc.__traceback__))

Agents throughout the codebase apply the @retry decorator with consistent three-attempt policies. In agents/script_enhancer.py, the decorator wraps async functions to catch transient failures:


# agents/script_enhancer.py

@retry(
    stop=stop_after_attempt(3),
    after=lambda rs: logging.warning(
        f"Retrying enhance_script due to error: {rs.outcome.exception()}"
    ),
)
async def enhance_script(...):
    ...

This pattern appears across multiple agents including scene_extractor, global_information_planner, and reranker_bge_silicon_api, providing standardized resilience without manual loop implementations.

Manual Exponential Backoff for HTTP 429

For APIs that return explicit rate-limit errors, tools/video_generator_veo_google_api.py implements a bespoke retry loop with exponential backoff specifically for HTTP 429 status codes:


# tools/video_generator_veo_google_api.py

max_retries = 3
retry_delay = 5
for attempt in range(max_retries):
    try:
        operation = self.client.models.generate_videos(**params, config=...)
        break
    except ClientError as e:
        if e.status_code == 429 and attempt < max_retries - 1:
            wait_time = retry_delay * (2 ** attempt)
            logging.warning(
                f"Rate limit hit (429), retrying in {wait_time}s..."
                f" (attempt {attempt + 1}/{max_retries})"
            )
            await asyncio.sleep(wait_time)
        else:
            raise

The Yunwu video generator (tools/video_generator_veo_yunwu_api.py) employs a similar pattern for network-level exceptions, demonstrating ViMax's adaptive approach to different failure modes.

Rate Limiting Strategies

While retries handle failures after they occur, ViMax proactively prevents quota exhaustion through a configurable rate limiting system.

The RateLimiter Class Implementation

The utils/rate_limiter.py file contains the RateLimiter class, which enforces per-minute and per-day request caps through timestamp tracking and async locking:


# utils/rate_limiter.py (excerpt)

async def acquire(self):
    if not self.max_requests_per_minute and not self.max_requests_per_day:
        return

    async with self.lock:
        now = time.time()
        # prune old timestamps…

        # daily limit check

        if self.max_requests_per_day and len(daily_requests) >= self.max_requests_per_day:
            wait = 86400 - (now - oldest_request)
            await asyncio.sleep(wait)
        # minute limit check

        if self.max_requests_per_minute and len(minute_requests) >= self.max_requests_per_minute:
            wait = 60 - (now - oldest_request)
            await asyncio.sleep(wait)
        # enforce minimum spacing (min_delay)

        if self.request_times and self.min_delay > 0:
            elapsed = now - self.request_times[-1]
            if elapsed < self.min_delay:
                await asyncio.sleep(self.min_delay - elapsed)
        self.request_times.append(time.time())

Key configuration parameters include:

  • max_requests_per_minute: Hard cap on requests within a 60-second sliding window
  • max_requests_per_day: Daily quota enforcement using 86400-second windows
  • min_delay: Minimum inter-request spacing to prevent burst traffic

Integrating Rate Limits into API Clients

ViMax uses optional dependency injection to wire rate limiting into API wrappers without coupling concerns. The Google video generator demonstrates this pattern by accepting an optional RateLimiter instance:


# tools/video_generator_veo_google_api.py (constructor)

def __init__(..., rate_limiter: Optional[RateLimiter] = None):
    self.rate_limiter = rate_limiter
    ...

# before sending a request

if self.rate_limiter:
    await self.rate_limiter.acquire()

This design allows any external service wrapper to opt-in to quota management by calling await self.rate_limiter.acquire() immediately before HTTP requests, ensuring consistent throttling across the entire system.

Combining Retry and Rate Limiting

For maximum resilience, ViMax components often layer both strategies. The request flow follows this sequence: first check rate limits (proactive), then execute the request, then retry if transient failures occur (reactive).

class ResilientAPI:
    def __init__(self, key: str, limiter: RateLimiter):
        self.key = key
        self.limiter = limiter

    @retry(stop=stop_after_attempt(3),
           after=lambda rs: logging.warning(f"Retry due to {rs.outcome.exception()}"))
    async def request(self, data: dict):
        await self.limiter.acquire()  # Block if quota exceeded

        # …make HTTP request…

This combination prevents unnecessary retry attempts when quotas are exhausted while ensuring transient network errors don't fail the entire workflow.

Practical Implementation Examples

Adding Retry to Custom Functions

To implement retry logic in new agents, import Tenacity decorators and define an appropriate stopping strategy:

from tenacity import retry, stop_after_attempt, wait_fixed
import logging

@retry(
    stop=stop_after_attempt(5),
    wait=wait_fixed(2),  # 2 seconds between attempts

    after=lambda rs: logging.warning(
        f"Retrying {rs.fn.__name__}{rs.outcome.exception()}"
    ),
)
async def fetch_something(url: str) -> dict:
    async with aiohttp.ClientSession() as s:
        async with s.get(url) as resp:
            resp.raise_for_status()
            return await resp.json()

Configuring Rate Limits for New Services

Instantiate the RateLimiter with appropriate quotas for your external API:

from utils.rate_limiter import RateLimiter

# Allow max 30 calls per minute, 500 per day

rl = RateLimiter(max_requests_per_minute=30, max_requests_per_day=500)

class MyExternalAPI:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.rate_limiter = rl

    async def expensive_call(self, payload: dict):
        await self.rate_limiter.acquire()  # Blocks if limit reached

        # …perform HTTP request…

Building Resilient API Clients

For APIs prone to both rate limiting and transient failures, combine both mechanisms:

from tenacity import retry, stop_after_attempt
from utils.rate_limiter import RateLimiter

class ProductionAPIClient:
    def __init__(self):
        self.rate_limiter = RateLimiter(
            max_requests_per_minute=60,
            min_delay=1.0
        )
    
    @retry(stop=stop_after_attempt(3))
    async def safe_request(self, endpoint: str):
        await self.rate_limiter.acquire()
        return await self._make_request(endpoint)

Summary

  • Tenacity decorators in utils/retry.py provide standardized retry logic with logging across all agents, typically configured for three attempts.
  • Manual exponential backoff handles HTTP 429 errors specifically in video generators, using configurable max_retries and retry_delay parameters.
  • RateLimiter class in utils/rate_limiter.py enforces granular quota controls via max_requests_per_minute, max_requests_per_day, and min_delay configurations.
  • Optional injection pattern allows API wrappers to adopt rate limiting without tight coupling, calling acquire() before external requests.
  • Layered resilience combines proactive rate limiting with reactive retry mechanisms to handle both quota exhaustion and transient network failures.

Frequently Asked Questions

How does ViMax handle HTTP 429 rate limit errors?

ViMax handles HTTP 429 errors through exponential backoff loops specifically implemented in API wrappers like tools/video_generator_veo_google_api.py. When a 429 status code is detected, the system calculates wait time using retry_delay * (2 ** attempt), logs the rate limit hit, and sleeps for the calculated duration before retrying, with a default maximum of three attempts.

What configuration options does the RateLimiter class support?

The RateLimiter class in utils/rate_limiter.py supports three primary configuration options: max_requests_per_minute for per-minute quota enforcement, max_requests_per_day for daily limits, and min_delay for enforcing minimum spacing between consecutive requests. The class maintains internal timestamp lists to track usage within sliding windows and automatically prunes stale entries.

How can I add retry logic to a new agent in ViMax?

Import the retry decorator and stop_after_attempt from Tenacity, then wrap your agent's main execution method. Follow the pattern used in agents/script_enhancer.py by including an after callback for logging. Use stop=stop_after_attempt(3) for consistency with existing agents, or adjust the number based on your specific reliability requirements.

Does the RateLimiter block the entire application or just the current request?

The RateLimiter blocks only the current coroutine calling acquire(), not the entire application. It uses async with self.lock to ensure thread-safe internal state management while allowing other tasks to continue execution. When limits are reached, the coroutine sleeps using await asyncio.sleep() for the calculated duration, yielding control back to the event loop.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →