Skip to content
Moeijiro
← All projects

Case study · API keys · rate limits · logs

API Management Platform

Developer console for an API product: API keys, per-key rate limits and a searchable log of every request.

Keys are stored as hashes and shown once; logging is batched off the request path.

APIs & IntegrationsSecurity & Monitoring
Type
Personal open-source project
Automated tests
38 (pytest)
Built with
Python · FastAPI · SQLAlchemy
Live demo
Runs locally — not hosted
Usage analytics: requests, success rate, latency and an hourly chart

Overview

What it does

Register, issue API keys, call a protected API with them, and see every request that was made — with its status, latency and the reason it was refused. It isn't an API gateway; it's the part of one that has to be right: key handling, quota enforcement and an audit trail you can query.

Problem

API products need keys that can't be leaked from the database, limits one customer can't use up for another, and logs that don't slow down the API they measure.

Solution

Keys are 256-bit random values stored only as SHA-256 digests. A middleware authenticates, applies a per-key sliding window and attaches X-RateLimit headers; request logs go onto an in-memory queue and are written in batches by a background task.

Screenshots

From the running application

  • Usage analytics: requests, success rate, latency and an hourly chart
    Usage analytics: requests, success rate, latency and an hourly chart
  • Request log with status, latency and refusal reason
    Request log with status, latency and refusal reason
  • API keys with prefixes and per-key limits
    API keys with prefixes and per-key limits
  • API documentation in the console
    API documentation in the console

Captured from the app running locally with its seeded demo data. Select an image to open it full size.

Architecture

How the pieces fit

ClientX-API-KeyDeveloper consolesession cookieKey middlewareSHA-256 lookupManagement API/api/*Rate limitersliding 60 s window/v1 endpoint+ X-RateLimit-*Log queuenon-blockingDatabasekeys · batched logs
  • Client → Key middleware
  • Developer console → Management API
  • Key middleware → Rate limiter
  • Rate limiter → /v1 endpoint
  • Rate limiter → Log queue
  • Log queue → Database
  • Management API → Database
  • Core service
  • External system
  • Storage / entry point

Core features

What's in it

  • API keys

    dev_live_… keys shown once, stored as a digest with a display prefix; revoke keeps history, delete keeps logs.

  • Per-key rate limits

    Sliding 60-second window, X-RateLimit-Limit/Remaining/Reset on every response and Retry-After on 429.

  • Request logs

    Key, method, path, status, latency, client IP and refusal code — never request bodies.

  • Usage analytics

    Requests today/this month, success rate, average latency and a 24-hour chart, computed from the log table.

  • Two auth schemes

    X-API-Key for machines on /v1, an HttpOnly session for humans on /api — a leaked key can't mint keys or read logs.

  • One error envelope

    Every failure, including validation errors, leaves as {"error": {"code", "message"}}.

Technical decisions

Interesting problems, and how they're solved

  1. 01

    Sliding window, per key

    One customer's burst can't spend another's budget, and a caller can't double the quota by straddling a minute boundary.

  2. 02

    Logging off the request path

    A non-blocking queue, one batched INSERT per flush, and dropped-and-counted logs if the queue fills — never a slower API.

  3. 03

    Digest lookup

    The raw key is never stored, logged or compared as a string; the digest makes the lookup one indexed query.

backend/app/core/rate_limit.py · L46–72View on GitHub ↗
def check(self, key: str, limit: int) -> RateLimitResult:
    now = time.monotonic()
    hits = self._hits[key]

    # Drop everything that has fallen out of the window.
    cutoff = now - self.window
    while hits and hits[0] <= cutoff:
        hits.popleft()

    if len(hits) >= limit:
        retry_after = max(1, int(self.window - (now - hits[0])) + 1)
        return RateLimitResult(
            allowed=False,
            limit=limit,
            remaining=0,
            reset_after=retry_after,
            retry_after=retry_after,
        )

    hits.append(now)
    reset_after = max(1, int(self.window - (now - hits[0]))) if hits else self.window
    return RateLimitResult(
        allowed=True,
        limit=limit,
        remaining=max(0, limit - len(hits)),
        reset_after=reset_after,
    )

Stack & skills

Built with

Technology

  • Python
  • FastAPI
  • SQLAlchemy
  • asyncio
  • PyJWT
  • React
  • TypeScript
  • Vite
  • Tailwind CSS

What this project demonstrates

  • API design
  • Rate limiting
  • Key management
  • Async background tasks
  • Observability

Repository & demo

Run it yourself

Register a local account, create a key and use make traffic to generate real requests against the demo API.

No public live demo is hosted. Everything below runs locally with Python 3.12+ and Node 20+.

$ git clone https://github.com/Moeijiro/api-management-platform.git
$ cd api-management-platform
$ make install
$ make api
$ make web
$ make traffic   # generate sample requests

Have a project in mind?

I build APIs, automation systems, Discord integrations and full-stack applications. Tell me what it should do and I'll tell you how I'd build it.