An API needs more than an endpoint that works. Once clients start using it, the service needs a way to identify callers, control how much traffic each can send, and show how the API is being used. I built an API rate limiter and key management platform as a small backend engineering MVP around those needs.
The project has two connected jobs. Its control plane manages tenants, rate limit policies, and API keys. Its gateway enforcement validates a key on a protected request, applies a Redis-backed limit, and records usage. Keeping those responsibilities clear made the system easier to reason about and test.
Separating durable configuration from live counters
I used FastAPI for the HTTP API, with SQLAlchemy and Alembic for database access and migrations. Postgres stores the information that should persist: tenants, rate limit policies, and hashed API keys. Each tenant can have a policy defined by requests_per_window and window_seconds.
Redis holds short-lived counters for request enforcement and usage tracking. A request should not need to write every hit to a long-lived relational table just to decide whether the next hit is allowed. The project uses these key shapes:
rl:{api_key_id}:{window_start_epoch}
usage:{api_key_id}:{YYYYMMDDHHMM}
The first identifies an API key’s rate limit window. The second groups usage into one-minute buckets. This division gives the control plane durable records while keeping frequently updated counts in Redis.

Managing the API key lifecycle
The control plane can create and list tenants, set or retrieve a tenant’s policy, and create, list, revoke, or rotate that tenant’s API keys. The lifecycle matters as much as issuing the first key: teams need to replace credentials and stop accepting old ones.
When a key is created, the API returns its plaintext value once. It stores a hash in Postgres, and list responses do not expose the plaintext. Rotation revokes the old key and returns a new plaintext key once. This gives a client a clear moment to save the credential while keeping routine management responses free of secret values.
At the protected endpoint, a missing or invalid X-API-Key returns 401. A valid key moves on to the rate limit check. These are distinct failures: one is about identity, the other is about the number of allowed requests.
Enforcing a fixed-window limit with Redis
The MVP uses a fixed-window counter. For a key’s current window, Redis INCR increments the count, and EXPIRE gives that window key a lifetime of the configured window length plus one second. If the count exceeds the tenant’s limit, the endpoint returns 429.
For example, with a policy of five requests per 60 seconds, the sixth request in that window is rejected. Both successful responses and 429 responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset, so a client can see the applicable limit and when it resets.

Fixed windows are simple to inspect and implement, which made them a sensible choice for this MVP. They also have a boundary tradeoff: a client can send requests at the end of one window and again at the start of the next. If a future version needs smoother enforcement, that behavior would be a reason to evaluate another algorithm.
Tracking usage and checking the workflow
The service also increments Redis usage counters in per-minute buckets. Its report endpoint can return a tenant’s usage for an explicit start and end range or for a recent period such as last_minutes=15. That gives the project a basic view of activity alongside enforcement.
I added Pytest integration tests and a GitHub Actions workflow covering linting, migrations, and tests. For local development, the repository includes Docker Compose for Postgres and Redis, an Alembic migration step, and FastAPI’s interactive API docs at /docs. The README has the setup commands and curl examples, including how to trigger a 429 and request a usage report.
What this MVP demonstrates
This project brings together several core backend engineering concerns: durable configuration, credential lifecycle, fast shared counters, predictable HTTP responses, and repeatable tests. It is an MVP, not a claim of a finished production gateway. Its value is in making each part visible and testable, from issuing a key to enforcing a policy and reviewing usage.
Explore the source code on GitHub if you want to see the implementation or run the API locally.