Skip to content

36 - Rate Limiting Guide

Overview

Tradeboard uses Flask-Limiter with a moving-window strategy to protect endpoints from abuse. Different rate limits apply to different endpoint categories based on their sensitivity and resource usage.

Two properties of the current setup matter before reading the numbers below. limiter.py passes no default_limits, so an endpoint without an explicit @limiter.limit(...) decorator is unlimited. And storage_uri is memory://, so counters live in the worker process; a multi-worker deployment enforces each limit once per worker rather than once per install. There are no limiter.exempt registrations anywhere in the codebase, and limiter.init_app(app) runs unconditionally in create_app().

Architecture Diagram

┌───────────────────────────────────────────────────────────────────────────────┐
│                          Rate Limiting Architecture                           │
└───────────────────────────────────────────────────────────────────────────────┘

                           Incoming Request


┌───────────────────────────────────────────────────────────────────────────────┐
│                                 Flask-Limiter                                 │
│                                                                               │
│  ┌─────────────────────────────────────────────────────────────────────────┐  │
│  │                      Configuration                                       │ │
│  │  key_func = get_remote_address   (Rate limit by IP)                     │  │
│  │  storage_uri = "memory://"       (In-memory storage)                    │  │
│  │  strategy = "moving-window"      (Sliding window algorithm)             │  │
│  └─────────────────────────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────────────────────┘


┌───────────────────────────────────────────────────────────────────────────────┐
│                          Endpoint Category Detection                          │
│                                                                               │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐              │
│  │   Login     │ │   API       │ │   Order     │ │  Webhook    │              │
│  │ Endpoints   │ │ Endpoints   │ │ Endpoints   │ │ Endpoints   │              │
│  └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘              │
│         │               │               │               │                     │
│         ▼               ▼               ▼               ▼                     │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐              │
│  │ 5/min       │ │ 50/sec      │ │ 10/sec      │ │ 100/min     │              │
│  │ 25/hour     │ │             │ │             │ │             │              │
│  └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘              │
└───────────────────────────────────────────────────────────────────────────────┘

                    ┌─────────────┴─────────────┐
                    │                           │
              Under Limit                  Over Limit
                    │                           │
                    ▼                           ▼
           ┌───────────────┐          ┌───────────────┐
           │   Process     │          │   429 Error   │
           │   Request     │          │ Too Many Reqs │
           └───────────────┘          └───────────────┘

Rate Limit Categories

Environment Variables

These are the eight keys .sample.env ships and utils/env_check.py validates:

bash
# Login endpoints (authentication security)
LOGIN_RATE_LIMIT_MIN = "5 per minute"
LOGIN_RATE_LIMIT_HOUR = "25 per hour"

# Password reset
RESET_RATE_LIMIT = "15 per hour"

# General API endpoints (data queries)
API_RATE_LIMIT="50 per second"

# Order endpoints (trading operations)
ORDER_RATE_LIMIT="10 per second"

# Smart order endpoints (automated trading)
SMART_ORDER_RATE_LIMIT="10 per second"

# Webhook endpoints (external integrations)
WEBHOOK_RATE_LIMIT="100 per minute"

# Strategy endpoints
STRATEGY_RATE_LIMIT="200 per minute"

RESET_RATE_LIMIT is read by blueprints/auth.py but is not in the rate_limit_vars list that utils/env_check.py validates, so a malformed value there is not caught at startup.

Limit Breakdown

CategoryRate LimitEndpointsPurpose
Login5/min, 25/hr/auth/login, /<broker>/callbackPrevent brute force
Password reset15/hr/auth/reset-passwordPrevent reset abuse
API50/sec/api/v1/quotes, /api/v1/positionbook, etc.General data access
Order10/sec/api/v1/placeorder, /api/v1/modifyorder, /api/v1/cancelorderTrading rate control
Smart Order10/sec/api/v1/placesmartorderAutomated order rate control
Webhook100/min/chartink/webhook, /strategy/webhookExternal integrations
Strategy200/minStrategy CRUD views in blueprints/strategy.py and blueprints/chartink.pyStrategy execution

Code Defaults Differ From The Sample Values

The number that applies when a key is absent from .env is the second argument to os.getenv in the module, not the value in .sample.env. For API_RATE_LIMIT the two disagree:

Default in codeModules
50 per secondblueprints/admin.py, blueprints/orders.py, blueprints/sandbox.py, restx_api/margin.py
10 per secondthe other 32 restx_api/*.py modules, including quotes.py, orderbook.py, holdings.py, funds.py, depth.py, history.py and cancel_all_order.py

Because .sample.env sets API_RATE_LIMIT="50 per second", a standard install gets 50/sec everywhere. Removing the key from .env silently drops most data endpoints to 10/sec. Keep the key present.

Additional Rate Limit Variables

These are read by code but are not part of the validated set. Several are absent from .sample.env altogether.

VariableDefault in codeApplied by
SIP_API_RATE_LIMIT10 per minuterestx_api/sip.py backtest POST
PORTFOLIO_API_RATE_LIMIT10 per minuterestx_api/portfolio.py backtest POST
PORTFOLIO_TEARSHEET_RATE_LIMIT5 per minuterestx_api/portfolio.py tearsheet
GREEKS_RATE_LIMIT30 per minuterestx_api/option_greeks.py
TELEGRAM_RATE_LIMIT30 per minuterestx_api/telegram_bot.py
WHATSAPP_RATE_LIMIT30 per minuterestx_api/whatsapp_bot.py
TELEGRAM_MESSAGE_RATE_LIMIT10 per minuteblueprints/telegram.py
WHATSAPP_MESSAGE_RATE_LIMIT10 per minuteblueprints/whatsapp.py
MCP_RATE_LIMIT_READ60 per minuteblueprints/mcp_http.py per-token scope quota
MCP_RATE_LIMIT_WRITE50 per minuteblueprints/mcp_http.py per-token scope quota

The MCP HTTP surface also carries two limits that are not environment-configurable: _DISPATCH_RATE_LIMIT = "120 per minute" on /mcp and _SSE_RATE_LIMIT = "5 per minute" on the SSE endpoint, both keyed by token rather than by remote address. None of these apply unless MCP_HTTP_ENABLED is True, since the blueprints are only registered in that case.

Implementation

Limiter Initialization

Location: limiter.py

python
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    key_func=get_remote_address,  # Rate limit by client IP
    storage_uri="memory://",       # In-memory storage
    strategy="moving-window"       # Sliding window algorithm
)

Applying Rate Limits

Login Endpoint Example:

python
# blueprints/auth.py
from limiter import limiter

LOGIN_RATE_LIMIT_MIN = os.getenv('LOGIN_RATE_LIMIT_MIN', '5 per minute')
LOGIN_RATE_LIMIT_HOUR = os.getenv('LOGIN_RATE_LIMIT_HOUR', '25 per hour')

@auth_bp.route('/login', methods=['GET', 'POST'])
@limiter.limit(LOGIN_RATE_LIMIT_MIN)
@limiter.limit(LOGIN_RATE_LIMIT_HOUR)
def login():
    # Multiple limits can stack (both must pass)
    ...

Order Endpoint Example:

python
# restx_api/place_order.py
from limiter import limiter

ORDER_RATE_LIMIT = os.getenv('ORDER_RATE_LIMIT', '10 per second')

@api.route('/', strict_slashes=False)
class PlaceOrder(Resource):
    @limiter.limit(ORDER_RATE_LIMIT)
    def post(self):
        """Place an order with the broker"""
        ...

API Endpoint Example:

python
# restx_api/quotes.py
from limiter import limiter

API_RATE_LIMIT = os.getenv('API_RATE_LIMIT', '10 per second')

@api.route('/', strict_slashes=False)
class Quotes(Resource):
    @limiter.limit(API_RATE_LIMIT)
    def post(self):
        """Get real-time quotes"""
        ...

Each module defines its own constant at import time. There is no shared constant, so the effective limit for a route is whatever that one module read from the environment when it was imported. Changing a rate limit therefore requires a restart, not just an .env edit.

Rate Limit Format

<number> per <timeunit>

Flask-Limiter also accepts compound limits joined by semicolons, and utils/env_check.py validates that form:

10 per second;40 per minute

Valid Timeunits

TimeunitAlias
seconds
minutem
hourh
dayd

Examples

bash
# Valid formats
5 per minute
10 per second
100 per hour
1000 per day

# Invalid formats (will fail validation)
5/minute        # Wrong separator
5 per minutes   # Wrong timeunit
five per minute # Must be number

Error Handling

429 Response Handler

Location: app.py

python
@app.errorhandler(429)
def rate_limit_exceeded(e):
    """Custom handler for 429 Too Many Requests"""
    from flask import redirect, request

    # Log rate limit hit
    logger.warning(f"Rate limit exceeded for {request.remote_addr}: {request.path}")

    # For API requests, return JSON response
    if request.path.startswith('/api/'):
        return {
            'status': 'error',
            'message': 'Rate limit exceeded. Please slow down your requests.',
            'retry_after': 60
        }, 429

    # For web requests, redirect to React rate-limited page
    return redirect('/rate-limited')

Client-Side Handling

python
# Python client example
import requests
import time

def place_order_with_retry(order_data, max_retries=3):
    for attempt in range(max_retries):
        response = requests.post(
            'http://localhost:5000/api/v1/placeorder',
            json=order_data,
            headers={'Authorization': f'Bearer {api_key}'}
        )

        if response.status_code == 429:
            retry_after = response.json().get('retry_after', 60)
            print(f"Rate limited. Waiting {retry_after}s...")
            time.sleep(retry_after)
            continue

        return response

    raise Exception("Max retries exceeded")

Endpoint Limits Map

REST API Endpoints

EndpointRate Limit VariableDefault
/api/v1/placeorderORDER_RATE_LIMIT10/sec
/api/v1/modifyorderORDER_RATE_LIMIT10/sec
/api/v1/cancelorderORDER_RATE_LIMIT10/sec
/api/v1/cancelallorderAPI_RATE_LIMIT50/sec (10/sec if the key is unset)
/api/v1/placesmartorderSMART_ORDER_RATE_LIMIT10/sec
/api/v1/quotesAPI_RATE_LIMIT50/sec
/api/v1/multiquotesAPI_RATE_LIMIT50/sec
/api/v1/positionbookAPI_RATE_LIMIT50/sec
/api/v1/orderbookAPI_RATE_LIMIT50/sec
/api/v1/tradebookAPI_RATE_LIMIT50/sec
/api/v1/holdingsAPI_RATE_LIMIT50/sec
/api/v1/fundsAPI_RATE_LIMIT50/sec
/api/v1/historyAPI_RATE_LIMIT50/sec
/api/v1/depthAPI_RATE_LIMIT50/sec
/api/v1/pingAPI_RATE_LIMIT50/sec
/api/v1/intervalsAPI_RATE_LIMIT50/sec
/api/v1/optionsmultiorderORDER_RATE_LIMIT10/sec

Authentication Endpoints

EndpointRate Limit VariableDefault
/auth/loginLOGIN_RATE_LIMIT_MIN + HOUR5/min, 25/hr
/auth/reset-passwordRESET_RATE_LIMIT15/hr
/<broker>/callbackLOGIN_RATE_LIMIT_MIN + HOUR5/min, 25/hr

blueprints/brlogin.py resolves its two constants through get_login_rate_limit_min() and get_login_rate_limit_hour() in utils/config.py, while blueprints/auth.py reads the same environment variables directly. The defaults match, so behaviour is identical, but the two paths are duplicated rather than sharing one source.

Webhook Endpoints

EndpointRate Limit VariableDefault
/chartink/webhookWEBHOOK_RATE_LIMIT100/min
/strategy/webhookWEBHOOK_RATE_LIMIT100/min

STRATEGY_RATE_LIMIT is applied to the strategy management views in blueprints/strategy.py and blueprints/chartink.py, not to the webhook receivers. The Flow webhook receivers /flow/webhook/<token> and /flow/webhook/<token>/<symbol> carry no @limiter.limit decorator at all; they are CSRF-exempt and unlimited, so front them with a reverse proxy limit if they are internet-facing.

Moving Window Strategy

┌──────────────────────────────────────────────────────────────────┐
│                      Moving Window Strategy                      │
└──────────────────────────────────────────────────────────────────┘

Time →  |-------- 1 minute window --------|
        ↓                                  ↓
        [==============================]

                                   Current time

As time advances, the window slides:
        |-------- 1 minute window --------|
                 ↓                         ↓
             [==============================]

Old requests fall out, new ones enter.
More accurate than fixed-window approach.

Algorithm Benefits

AspectMoving WindowFixed Window
AccuracyHigherLower
Burst protectionBetterProne to bursts at boundaries
MemorySlightly higherLower
ImplementationMore complexSimpler

Configuration Validation

Location: utils/env_check.py

python
rate_limit_vars = [
    "LOGIN_RATE_LIMIT_MIN",
    "LOGIN_RATE_LIMIT_HOUR",
    "API_RATE_LIMIT",
    "ORDER_RATE_LIMIT",
    "SMART_ORDER_RATE_LIMIT",
    "WEBHOOK_RATE_LIMIT",
    "STRATEGY_RATE_LIMIT",
]
# Single: "10 per second"
# Compound (Flask-Limiter syntax): "10 per second;40 per minute"
single_limit = r"\d+\s+per\s+(second|minute|hour|day)"
rate_limit_pattern = re.compile(
    rf"^{single_limit}(;{single_limit})*$"
)

for var in rate_limit_vars:
    value = os.getenv(var, "")
    if not rate_limit_pattern.match(value):
        print(f"\nError: Invalid {var} format.")
        print("Format should be: 'number per timeunit'")
        print("Compound limits use semicolons: 'number per timeunit;number per timeunit'")
        print("Examples: '5 per minute', '10 per second', '10 per second;40 per minute'")
        sys.exit(1)

Validation is fail-fast: an unset or malformed value in that list calls sys.exit(1) before the Flask app is built. RESET_RATE_LIMIT and every variable in the additional table above are outside this check.

Tuning Recommendations

For High-Frequency Trading

bash
# Increase order limits for HFT
ORDER_RATE_LIMIT=50 per second
SMART_ORDER_RATE_LIMIT=10 per second
API_RATE_LIMIT=200 per second

For Webhook-Heavy Usage

bash
# Increase webhook limits for multiple signal sources
WEBHOOK_RATE_LIMIT=500 per minute
STRATEGY_RATE_LIMIT=1000 per minute

For Multi-User Deployments

Consider using Redis for distributed rate limiting:

python
# limiter.py (with Redis)
limiter = Limiter(
    key_func=get_remote_address,
    storage_uri="redis://localhost:6379",
    strategy="moving-window"
)

Key Files Reference

FilePurpose
limiter.pyFlask-Limiter construction
app.pylimiter.init_app(app) and the 429 error handler
utils/env_check.pyRate limit format validation at startup
utils/config.pyget_login_rate_limit_min(), get_login_rate_limit_hour()
restx_api/*.pyAPI endpoint rate limits
blueprints/auth.pyLogin and password-reset rate limits
blueprints/brlogin.pyBroker callback rate limits
blueprints/chartink.pyChartink webhook and strategy rate limits
blueprints/strategy.pyStrategy webhook and strategy rate limits
blueprints/mcp_http.pyRemote MCP dispatch, SSE and per-scope quotas