Rate Limits
NinjasProxy does not impose artificial request-per-second throttles. Your throughput is governed by three practical constraints: concurrent connections allowed by your plan, the per-GB bandwidth billing model, and your account balance.
Concurrent Connections
Each open TCP connection to the proxy gateway counts against your concurrent-connection allowance. New connections beyond this limit receive a 503 Service Unavailable or a TCP reset until a slot frees up.
| Plan | Max concurrent connections |
|---|---|
| Starter | 50 |
| Growth | 200 |
| Pro | 500 |
| Enterprise | Custom (contact sales) |
Request Rate
There is no separate requests-per-second (RPS) quota. Your effective RPS is determined naturally by:
- The number of concurrent connections your plan allows
- The latency of each request (varies by proxy type, geo, and target site)
- Connection reuse — HTTP/1.1 keep-alive or HTTP/2 multiplexing reduces connection overhead
A practical formula: Max RPS ≈ Concurrent Connections / Average Request Latency (s). With 200 concurrent connections and an average 1 s latency, you can sustain ~200 RPS.
Bandwidth Billing
Bandwidth is billed per-GB of data sent through the proxy (request + response bytes combined). There is no artificial bandwidth throttle — your connection is only limited by peer network capacity (typically 100 Mbps+ per residential/mobile peer).
- Datacenter proxies: unlimited bandwidth (fixed monthly rate)
- Residential proxies: billed per GB consumed
- Mobile proxies: billed per GB consumed (higher per-GB rate than residential)
What Happens When Balance Runs Out
When your prepaid balance reaches zero, new proxy requests are rejected immediately with a 402 Payment Required-equivalent response from the gateway. Existing open connections are not mid-stream terminated, but no new connections are accepted until the balance is topped up.
# Balance-exhausted response from the proxy gateway
HTTP/1.1 402 Payment Required
X-NinjasProxy-Error: insufficient_balance
Content-Type: application/json
{"error": "insufficient_balance", "message": "Top up your balance at portal.ninjasproxy.com"}Best Practices
Connection pooling
Reuse connections with HTTP keep-alive instead of opening a fresh TCP connection per request. Most HTTP clients do this automatically when you reuse the session object:
import requests
# ✅ Good — one session, connection pool reused
session = requests.Session()
session.proxies = {
"http": "http://USERNAME:API_KEY@r.ninjasproxy.com:8080",
"https": "http://USERNAME:API_KEY@r.ninjasproxy.com:8080",
}
for url in urls:
r = session.get(url, timeout=30)
# ❌ Bad — new TCP connection for every request
for url in urls:
r = requests.get(url, proxies=proxies, timeout=30)Retry logic with exponential backoff
Transient failures (peer disconnect, target 5xx, connection timeout) are normal in distributed proxy networks. Wrap requests in a retry loop with exponential backoff:
import requests
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
retry_if_result,
before_sleep_log,
)
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
PROXIES = {
"http": "http://USERNAME:API_KEY@r.ninjasproxy.com:8080",
"https": "http://USERNAME:API_KEY@r.ninjasproxy.com:8080",
}
def is_retryable_status(response: requests.Response) -> bool:
"""Retry on 5xx errors and 407 (proxy auth glitch), but not on 402 (balance)."""
return response.status_code in {500, 502, 503, 504, 407}
@retry(
retry=(
retry_if_exception_type((requests.ConnectionError, requests.Timeout))
| retry_if_result(is_retryable_status)
),
wait=wait_exponential(multiplier=1, min=2, max=30),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def fetch(url: str, session: requests.Session) -> requests.Response:
return session.get(url, proxies=PROXIES, timeout=30)
# Usage
with requests.Session() as s:
for url in urls:
try:
r = fetch(url, s)
if r.status_code == 402:
raise RuntimeError("Balance exhausted — top up at portal.ninjasproxy.com")
print(r.status_code, url)
except Exception as e:
print(f"Failed after retries: {url} — {e}")Adaptive concurrency
If you're running many workers, use a semaphore to cap concurrency below your plan limit, leaving headroom for retries:
import asyncio
import httpx
MAX_CONCURRENT = 150 # Stay below your plan limit
async def scrape(url: str, client: httpx.AsyncClient, sem: asyncio.Semaphore) -> str:
async with sem:
r = await client.get(url, timeout=30)
r.raise_for_status()
return r.text
async def main(urls: list[str]):
proxy = "http://USERNAME:API_KEY@r.ninjasproxy.com:8080"
sem = asyncio.Semaphore(MAX_CONCURRENT)
async with httpx.AsyncClient(proxy=proxy) as client:
tasks = [scrape(url, client, sem) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
for url, result in zip(urls, results):
if isinstance(result, Exception):
print(f"Error {url}: {result}")
else:
print(f"OK {url}: {len(result)} bytes")
asyncio.run(main(["https://example.com"] * 500))Next Steps
- Authentication — API key and IP whitelist setup
- Python integration — full async + tenacity patterns
- API Reference — poll usage stats programmatically