> ## Documentation Index
> Fetch the complete documentation index at: https://docs.featherframework.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limiting

> A decorator for single-process guards, and Flask-Limiter with Redis for limits that actually hold across workers.

## The decorator

```python theme={null}
from feather.auth import rate_limit

# 5 login attempts per minute per IP
@api.post('/login')
@rate_limit(5, 60)
def login():
    pass

# 100 API calls per minute per authenticated user
@api.get('/search')
@rate_limit(100, 60, key='user')
def search():
    pass

# limit by both IP and user
@api.post('/expensive')
@rate_limit(10, 3600, key='ip+user')
def expensive_operation():
    pass

@api.post('/comments')
@rate_limit(10, 3600, message='You can only post 10 comments per hour')
def create_comment():
    pass
```

| Parameter | Description                              | Default               |
| --------- | ---------------------------------------- | --------------------- |
| `limit`   | Max requests in the period               | required              |
| `period`  | Time window in seconds                   | 60                    |
| `key`     | Limit by `'ip'`, `'user'` or `'ip+user'` | `'ip'`                |
| `message` | Custom error message                     | "Rate limit exceeded" |

<Warning>
  `@rate_limit` keeps its counters in the process. Under `gunicorn --workers 4` a limit
  of ten per minute is really forty per minute. Treat it as a development guard and a
  convenience for single-process deployments, not as protection.
</Warning>

## In production: Flask-Limiter

Scaffolded apps with authentication ship a `rate_limits.py` that does this properly. It
limits the Google OAuth login and callback and every admin POST route through
Flask-Limiter, sharing counters across workers via Redis.

```bash theme={null}
pip install "feather-framework[ratelimit]"
```

```bash .env theme={null}
RATELIMIT_STORAGE_URI=redis://localhost:6379/1
```

Without it the limiter falls back to memory and logs a warning saying so. The limits
themselves live in `config.py` as `RATELIMIT_DEFAULT`, `RATELIMIT_LOGIN` and
`RATELIMIT_ADMIN`, each overridable by environment variable. Run
`flask limiter limits` to print which routes are actually limited.

### Two things that fail silently

Both cost real debugging time in production if you wire this up by hand.

<AccordionGroup>
  <Accordion title="Assign the wrapper back" icon="circle-alert">
    Flask-Limiter enforces a limit only through the function it returns. Writing
    `limiter.limit(rule)(app.view_functions[ep])` and discarding the result does
    nothing — and worse, drops that endpoint out of the default limit too.

    ```python theme={null}
    app.view_functions[ep] = limiter.limit(rule)(view)
    ```
  </Accordion>

  <Accordion title="Exempt static endpoints" icon="circle-alert">
    One page load fetches ten or more scripts and fonts from Flask. Without a
    `limiter.request_filter` exempting `static` and `feather_static`, a busy user gets a
    429 on the app's own JavaScript while the page is still loading.
  </Accordion>
</AccordionGroup>
