Configurable mock API server with realistic rate limiting for testing
Project description
mocklimit
A configurable mock API server that simulates rate limiting. Point it at an OpenAPI spec and a YAML config, and you get a local server that responds like a real rate-limited API. Useful for testing backpressure, retry logic, and admission control without hitting production.
Installation
pip install mocklimit
Or with uv:
uv add mocklimit
Quick start
1. Write a rate limit config
# limits.yaml
policies:
chat:
strategy: fixed_window
limits:
- dimension: requests
limit: 60
window_seconds: 60
headers:
limit: x-ratelimit-limit-requests
remaining: x-ratelimit-remaining-requests
reset: x-ratelimit-reset-requests
- dimension: tokens
limit: 150000
window_seconds: 60
headers:
limit: x-ratelimit-limit-tokens
remaining: x-ratelimit-remaining-tokens
reset: x-ratelimit-reset-tokens
scope: api_key
format:
reset: go_duration
retry_after:
header: retry-after-ms
unit: milliseconds
error_template:
provider: openai
endpoints:
/chat/completions:
methods: [POST]
policy: chat
resources:
requests:
input:
strategy: fixed
value: 1
output:
strategy: fixed
value: 0
tokens:
input:
strategy: characters_div_4
output:
strategy: random
range: [50, 500]
timing:
base_ms: [20, 100]
scale:
resource: tokens
component: output
ms_per_unit: 0.02
2. Start the server
mocklimit serve --spec openapi.yaml --rate-config limits.yaml
The server reads your OpenAPI spec for route definitions and response schemas, then applies rate limiting per the config. Once the limit is reached, requests get a 429 with the configured headers and error body.
3. CLI options
mocklimit serve --spec <path> --rate-config <path> [--host HOST] [--port PORT]
| Flag | Default | Description |
|---|---|---|
--spec |
(required) | Path to OpenAPI spec (YAML) |
--rate-config |
(required) | Path to rate limit config (YAML) |
--host |
127.0.0.1 |
Host to bind to |
--port |
8000 |
Port to listen on |
Features
- Parses your OpenAPI spec and registers all endpoints with dummy responses
- Three rate limiting algorithms: fixed window, sliding window, token bucket
- Multiple limits per endpoint (e.g. RPM + TPM + input TPM)
- Per-limit header configuration (each limit can emit its own header group)
- Configurable reset format: relative seconds, Go-style duration, RFC 3339 timestamps
- Configurable retry-after: header name and unit (seconds or milliseconds)
- Provider-accurate 429 error bodies (OpenAI, Anthropic, Google templates)
- Configurable resource estimation with input/output components
- Response latency simulation with output-proportional scaling
- Per-key scoping by API key or IP address
- Request statistics via
/mocklimit/stats - Prometheus metrics at
/metrics
Config reference
Policies
A policy defines how a group of endpoints are rate limited.
policies:
my_policy:
strategy: fixed_window # or: sliding_window, token_bucket
limits: [...] # list of limit definitions
scope: api_key # or: ip
format: # optional, controls header value formatting
reset: relative_seconds # or: go_duration, rfc3339
retry_after:
header: Retry-After # header name for retry signal
unit: seconds # or: milliseconds
headers: # optional fallback headers (used when limits don't define their own)
limit: x-ratelimit-limit
remaining: x-ratelimit-remaining
reset: x-ratelimit-reset
error_template: # optional, controls 429 response body
provider: openai # or: anthropic, google
response_latency_ms: [0, 0] # legacy latency range, prefer endpoint timing
Limits
Each policy has one or more limits. A limit tracks usage of a single dimension.
For fixed window and sliding window:
limits:
- dimension: requests # name of the resource to track
limit: 60 # max allowed per window
window_seconds: 60 # window duration
headers: # optional, per-limit header names
limit: x-ratelimit-limit-requests
remaining: x-ratelimit-remaining-requests
reset: x-ratelimit-reset-requests
For token bucket:
limits:
- dimension: tokens.output # supports dotted notation for components
capacity: 16000 # bucket capacity
refill_rate: 266 # tokens per second refill
headers:
limit: anthropic-ratelimit-output-tokens-limit
remaining: anthropic-ratelimit-output-tokens-remaining
reset: anthropic-ratelimit-output-tokens-reset
The dimension field references a resource from the endpoint config. Use dotted notation to track a specific component: tokens.input tracks only the input component, tokens.output tracks only the output, and plain tokens tracks the total (input + output).
Endpoints
Each endpoint maps an API path to a policy and defines how to estimate resource costs.
endpoints:
/chat/completions:
methods: [POST]
policy: chat
resources:
requests:
input:
strategy: fixed
value: 1
output:
strategy: fixed
value: 0
tokens:
input:
strategy: characters_div_4
output:
strategy: random
range: [50, 500]
timing:
base_ms: [20, 100]
scale:
resource: tokens
component: output
ms_per_unit: 0.02
Resources
Each resource has an input and output component. The total cost is input + output. Every component has a strategy:
| Strategy | Config | Behavior |
|---|---|---|
fixed |
value: N |
Always returns N |
random |
range: [min, max] |
Uniform random integer between min and max |
characters_div_4 |
(none) | len(request_body) // 4, rough token estimate |
The requests resource is typically fixed: 1 input with fixed: 0 output, so each request costs 1. The tokens resource usually uses characters_div_4 for input and random for output, simulating variable LLM response lengths.
Timing
Controls how long responses take. Useful for simulating real API latency.
timing:
base_ms: [20, 100] # random base delay in this range
scale:
resource: tokens # scale proportionally to this resource
component: output # use the output component
ms_per_unit: 0.02 # add 0.02ms per output token
A request with 500 output tokens and base range [20, 100] would take roughly 20-100ms base + 10ms scaling = 30-110ms total.
Reset formats
The format.reset field controls how the reset header value is formatted:
| Format | Example | Used by |
|---|---|---|
relative_seconds |
4.3 |
Default, plain seconds until reset |
go_duration |
12ms, 4.253s, 1m0s |
OpenAI |
rfc3339 |
2026-06-07T15:30:00Z |
Anthropic |
Retry-after
The format.retry_after block controls the 429 retry signal:
retry_after:
header: retry-after-ms # header name (default: "Retry-After")
unit: milliseconds # seconds or milliseconds
Error templates
When error_template.provider is set, 429 responses include a realistic error body instead of the dummy response. The bodies below are returned as JSON.
openai:
error:
message: "Rate limit reached for tokens in organization on tokens per min (TPM): Limit 150000, Used 150000, Requested 1. Please retry after 6m0s."
type: tokens
param: null
code: rate_limit_exceeded
anthropic:
type: error
error:
type: rate_limit_error
message: "Number of requests has exceeded your rate limit. Please retry after 5 seconds."
google:
error:
code: 429
message: "Resource has been exhausted (e.g. check quota). Please retry in 3.500000s."
status: RESOURCE_EXHAUSTED
details:
- "@type": type.googleapis.com/google.rpc.QuotaFailure
violations:
- quotaMetric: generativelanguage.googleapis.com/requests_count
quotaLimit: "15"
quotaDimensions:
model: unknown
- "@type": type.googleapis.com/google.rpc.RetryInfo
retryDelay: "3.500000s"
Header behavior
Headers are emitted on every response (200 and 429). There are two ways to configure them:
-
Per-limit headers: each limit in the policy defines its own header names. All configured header groups are emitted simultaneously. This is what Anthropic and OpenAI do (separate request and token headers).
-
Policy-level fallback headers: a single header group on the policy, used with whichever limit is most restrictive. Simpler, but you only see one dimension.
If no headers are configured on any limit or the policy, no rate-limit headers are emitted (Google Gemini behavior, which only signals limits in the 429 error body).
Full example configs
See tests/server/configs/ for complete working examples:
anthropic.yaml- token bucket, RFC 3339 reset, 3 separate header groups, Anthropic error bodyopenai.yaml- fixed window, Go-style duration, 2 header groups, millisecond retry-after, OpenAI error bodygemini.yaml- fixed window, no success headers, input-only TPM, Google error body
Prometheus metrics
Exposed at /metrics/.
| Metric | Type | Labels | Description |
|---|---|---|---|
mocklimit_requests_total |
Counter | endpoint, method, scope_key, status |
Total requests |
mocklimit_rate_limited_total |
Counter | endpoint, method, scope_key |
Requests denied (429) |
mocklimit_request_duration_seconds |
Histogram | endpoint, method, status |
Response latency |
mocklimit_rate_limit_remaining |
Gauge | endpoint, policy, scope_key |
Remaining budget |
Scrape config:
scrape_configs:
- job_name: mocklimit
metrics_path: /metrics/
static_configs:
- targets: ["localhost:8000"]
Programmatic usage
For use in tests without starting a subprocess:
from mocklimit.server import create_app
app = create_app(spec_path="openapi.yaml", rate_config_path="limits.yaml")
This returns a FastAPI app you can use with any ASGI test client (httpx, starlette TestClient, etc).
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mocklimit-0.6.0.tar.gz.
File metadata
- Download URL: mocklimit-0.6.0.tar.gz
- Upload date:
- Size: 23.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5aa2393dbc0d32c7e12d6807f73e324e36b1643939df14492754f9c2207bdfe4
|
|
| MD5 |
52020fb9946f07b166fe216d543f81c9
|
|
| BLAKE2b-256 |
c648025e405762a24d112f6f9ef68a94f836bc13b3508c76788f89a6382fa90c
|
File details
Details for the file mocklimit-0.6.0-py3-none-any.whl.
File metadata
- Download URL: mocklimit-0.6.0-py3-none-any.whl
- Upload date:
- Size: 33.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be881814181db97ccc0c426d687a9227b069fd7ffdf88ea61599bb168a5d7c13
|
|
| MD5 |
a5a90411dd01271527c06c48f35a70fa
|
|
| BLAKE2b-256 |
ffea18a71431785f616c0ae811374adb67901f3b914b582e55e447f73d0b4e99
|