How It Works Features Pricing Blog Error Guides
Log In Start Free Trial
Flask · Python

Fix RateLimitExceeded: 429 Too Many Requests: Rate limit exceeded in Flask

This error occurs when Flask-Limiter detects too many requests from a client within the configured time window. It protects your API from abuse but can block legitimate users if misconfigured. Fix it by adjusting rate limits, implementing per-user limits with proper key functions, and adding a custom 429 error handler with Retry-After headers.

Reading the Stack Trace

Traceback (most recent call last): File "/app/venv/lib/python3.12/site-packages/flask/app.py", line 869, in full_dispatch_request rv = self.dispatch_request() File "/app/venv/lib/python3.12/site-packages/flask_limiter/extension.py", line 445, in __check_request_limit raise RateLimitExceeded(limit) File "/app/venv/lib/python3.12/site-packages/flask_limiter/errors.py", line 16, in __init__ super().__init__(description=description) flask_limiter.errors.RateLimitExceeded: 429 Too Many Requests: 10 per 1 minute

Here's what each line means:

Common Causes

1. Rate limit too aggressive

The global default rate limit is too low for normal usage patterns, blocking legitimate users.

from flask_limiter import Limiter
limiter = Limiter(app, default_limits=['10 per minute'])  # too restrictive

2. Key function uses shared IP behind proxy

All users behind a shared proxy or load balancer share the same IP, so they collectively exhaust the limit.

limiter = Limiter(app, key_func=get_remote_address)
# All users behind a corporate proxy share 1 IP

3. No custom 429 error handler

The app does not handle the 429 response, so clients receive a bare HTML error page with no retry guidance.

# No @app.errorhandler(429) registered

The Fix

Use a custom key function that rate-limits by authenticated user ID rather than IP address, preventing shared-proxy collisions. Increase the default limit to a reasonable threshold and add a 429 error handler that includes a Retry-After header so clients know when to retry.

Before (broken)
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(app, key_func=get_remote_address, default_limits=['10 per minute'])
After (fixed)
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

def get_rate_limit_key():
    if hasattr(g, 'current_user') and g.current_user:
        return f'user:{g.current_user.id}'
    return get_remote_address()

limiter = Limiter(app, key_func=get_rate_limit_key, default_limits=['200 per minute'])

@app.errorhandler(429)
def ratelimit_handler(e):
    return jsonify({
        'error': 'Rate limit exceeded',
        'retry_after': e.description
    }), 429, {'Retry-After': '60'}

Testing the Fix

import pytest
from app import create_app

@pytest.fixture
def client():
    app = create_app()
    app.config['TESTING'] = True
    app.config['RATELIMIT_ENABLED'] = True
    app.config['RATELIMIT_STORAGE_URI'] = 'memory://'
    return app.test_client()

def test_requests_within_limit_succeed(client):
    for _ in range(5):
        response = client.get('/api/data')
        assert response.status_code == 200

def test_429_includes_retry_after(client):
    # Temporarily set very low limit for testing
    app = client.application
    for _ in range(201):
        response = client.get('/api/data')
    assert response.status_code == 429
    assert 'Retry-After' in response.headers

Run your tests:

pytest tests/ -v

Pushing Through CI/CD

git checkout -b fix/flask-rate-limit-tuning,git add app/__init__.py app/routes.py,git commit -m "fix: adjust rate limits and add 429 error handler with Retry-After",git push origin fix/flask-rate-limit-tuning

Your CI config should look something like this:

name: CI
on:
  pull_request:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v --tb=short
      - run: flake8 app/

The Full Manual Process: 18 Steps

Here's every step you just went through to fix this one bug:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. Monitor production to confirm the error is resolved

Total time: 30-60 minutes. For one bug.

Or Let bugstack Fix It in Under 2 minutes

Every step above? bugstack does it automatically.

Step 1: Install the SDK

pip install bugstack

Step 2: Initialize

import bugstack

bugstack.init(api_key=os.environ["BUGSTACK_API_KEY"])

Step 3: There is no step 3.

bugstack handles everything from here:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. Deploys to production (or opens a PR for review)

Time from error to fix deployed: Under 2 minutes.

Human involvement: zero.

Try bugstack Free →

No credit card. 5-minute setup. Cancel anytime.

Deploying the Fix (Manual Path)

  1. Run pytest locally to verify rate limiting works as expected.
  2. Open a pull request with the updated limiter config and error handler.
  3. Wait for CI checks to pass on the PR.
  4. Have a teammate review and approve the PR.
  5. Merge to main and monitor 429 rates in staging to fine-tune limits.

Frequently Asked Questions

BugStack tests requests at and beyond the rate limit boundary, verifies the Retry-After header is present, and runs your full test suite before marking it safe.

BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.

Use Redis in production for shared state across workers. Memory storage only works for single-process deployments.

Decorate those routes with @limiter.exempt or apply specific higher limits with @limiter.limit('1000 per minute').