Extension Guide¶
Limify is designed to be extensible.
Its architecture separates:
- framework integration
- request resolution
- rate-limiting logic
- storage persistence
This makes it possible to extend Limify without modifying the core behavior.
Typical extension points include:
- custom algorithms
- custom storage adapters
- custom plan providers
- custom framework adapters
This guide focuses on the two main extension points:
- custom algorithms
- custom storage adapters
Extension Philosophy¶
Limify follows a clean architecture approach.
The core idea is:
- algorithms should not depend on frameworks
- storage should not depend on business logic
- adapters should isolate external systems
This means each extension should have a single responsibility.
For example:
- an algorithm decides whether a request is allowed
- a storage adapter persists and retrieves state
- a framework adapter translates HTTP requests into Limify context objects
Keeping those boundaries clear makes extensions easier to test and maintain.
Custom Algorithm¶
A custom algorithm is useful when Token Bucket is not the best fit for your traffic model.
Examples of alternative algorithms:
- Sliding Window
- Fixed Window
- Leaky Bucket
- concurrency limits
- quota-based limits
A custom algorithm should implement the same role as the built-in algorithms:
- receive the resolved key and plan
- retrieve/update state through the storage adapter
- return the rate-limit decision
What an Algorithm Needs¶
A Limify algorithm typically works with:
- a key that uniquely identifies the rate limit bucket
- a plan containing:
idlimitperiod_seconds- a storage adapter
- the current timestamp
The algorithm should return a result that indicates:
- whether the request is allowed
- how many requests remain
- how long until reset or retry
This keeps the algorithm independent of frameworks and request objects.
Responsibilities of an Algorithm¶
A custom algorithm should be responsible for:
- interpreting the rate limit values
- reading current state
- updating state
- deciding allow/block
- returning structured output
A custom algorithm should not be responsible for:
- HTTP responses
- request parsing
- identity selection
- plan lookup
- framework-specific behavior
Those responsibilities belong to other layers.
Example: Custom Fixed Window Algorithm¶
Below is a simplified conceptual example:
import time
from dataclasses import dataclass
@dataclass
class LimitationResult:
allowed: bool
remaining: int
reset_after: int
class FixedWindowAlgorithm:
def __init__(self, storage):
self.storage = storage
def check(self, key, plan):
now = int(time.time())
window_start = now - (now % plan.period_seconds)
window_key = f"{key}:{window_start}"
current = self.storage.get(window_key) or 0
if current >= plan.limit:
reset_after = plan.period_seconds - (now % plan.period_seconds)
return LimitationResult(
allowed=False,
remaining=0,
reset_after=reset_after,
)
self.storage.increment(window_key, ttl=plan.period_seconds)
return LimitationResult(
allowed=True,
remaining=max(plan.limit - current - 1, 0),
reset_after=plan.period_seconds - (now % plan.period_seconds),
)
This example is intentionally simplified, but it shows the core idea:
- calculate the active window
- count requests in that window
- reject if the limit is reached
- otherwise increment the count
Async Custom Algorithm¶
For async applications, your algorithm may need async methods.
Example shape:
class AsyncCustomAlgorithm:
def __init__(self, storage):
self.storage = storage
async def check(self, key, plan):
...
This is similar to Limify’s AsyncTokenBucketAlgorithm.
Using the Storage Adapter¶
Algorithms should rely on the storage adapter instead of talking directly to Redis or another backend.
Why:
- easier to test
- easier to replace storage backends
- cleaner architecture
- consistent interface
For example, the built-in token bucket algorithm uses the storage adapter to:
- load a Lua script
- execute it atomically
- retrieve the updated bucket state
Your custom algorithm should follow the same principle.
Testing a Custom Algorithm¶
A custom algorithm should be tested independently from framework code.
Typical tests should verify:
- request allowed when under limit
- request blocked when limit exceeded
- state updates correctly
- retry/reset values are correct
- storage failures are handled properly
Because the algorithm is isolated, it should be possible to test it using:
- mocked storage adapters
- fake storage backends
- deterministic timestamps
Custom Storage¶
A custom storage adapter allows Limify to persist rate-limit state in a backend other than Redis.
Examples of possible storage backends:
- in-memory storage
- PostgreSQL
- Memcached
- distributed cache services
- custom internal infrastructure
Storage adapters are responsible for providing the persistence operations needed by algorithms.
Why Storage Is Abstracted¶
Limify algorithms should not depend on:
- Redis client details
- connection handling
- sync vs async command syntax
- backend-specific APIs
Instead, they depend on an abstraction.
This allows the same algorithm design to work across different storage systems.
Current Built-In Storage Adapters¶
Limify currently includes:
RedisSyncAdapterRedisAsyncAdapter
These adapters provide the persistence and script execution functionality required by the built-in token bucket algorithm.
Responsibilities of a Storage Adapter¶
A storage adapter should be responsible for:
- communicating with the backend
- reading and writing state
- exposing a clean interface to algorithms
- hiding backend-specific implementation details
A storage adapter should not contain rate-limiting policy logic.
That logic belongs in the algorithm.
Minimal Redis-Compatible Interface¶
Your current token bucket algorithm expects Redis-oriented behavior, including:
script_load(...)evalsha(...)
That is because the built-in token bucket implementation uses Lua for atomic execution.
So if you want to support the existing token bucket algorithm with another backend, you would need an adapter that can provide equivalent behavior.
Example: In-Memory Storage Adapter¶
For simpler algorithms or testing, you can build an in-memory adapter.
Example:
class InMemoryStorage:
def __init__(self):
self.data = {}
def get(self, key):
return self.data.get(key)
def set(self, key, value):
self.data[key] = value
def increment(self, key, ttl=None):
self.data[key] = self.data.get(key, 0) + 1
return self.data[key]
This is useful for:
- testing
- local development
- prototypes
However, it is not suitable for distributed production systems, because each process would maintain separate state.
Matching Storage to Algorithm¶
Not every storage adapter fits every algorithm.
Examples:
Token Bucket with Redis Lua¶
Best suited for:
- Redis
- atomic scripting support
- distributed systems
Fixed Window with In-Memory Storage¶
Best suited for:
- local development
- testing
- single-process applications
So when building a custom algorithm or adapter, think of them as a pair:
- algorithm design
- storage capability
Designing a Clean Adapter Interface¶
If you plan to support multiple backends in the future, it may be useful to define a formal adapter protocol or base class.
For example:
class StorageAdapter:
def get(self, key):
raise NotImplementedError
def set(self, key, value):
raise NotImplementedError
def increment(self, key, ttl=None):
raise NotImplementedError
Or for Redis-script-based algorithms:
class ScriptStorageAdapter:
def script_load(self, script: str):
raise NotImplementedError
def evalsha(self, sha: str, num_keys: int, *args):
raise NotImplementedError
This makes extension more predictable and easier to document.
Custom Framework Adapters¶
Although this page focuses on algorithms and storage, Limify can also be extended with new framework adapters.
A framework adapter should:
- intercept requests
- extract method/path/client metadata
- build a
RequestContext - call the limiter
- enforce the result in framework-specific form
This is how the FastAPI/Starlette middleware works.
Future adapters could target:
- Flask
- Django
- generic ASGI
- API gateway integrations
Best Practices for Extensions¶
When extending Limify:
Keep responsibilities narrow¶
Do not mix framework logic, algorithm logic, and storage code in the same component.
Reuse existing abstractions¶
Use RequestContext, Plan, and Limify’s core flow instead of bypassing them.
Prefer deterministic behavior¶
Rate limiting should behave consistently under concurrency and load.
Test extensions independently¶
A custom algorithm or adapter should be testable in isolation.
Document assumptions clearly¶
If your algorithm depends on atomic storage operations, note that explicitly.
Summary¶
Limify is designed to be extended in a clean and predictable way.
You can extend it by adding:
- custom algorithms
- custom storage adapters
- custom plan providers
- custom framework adapters
A custom algorithm should focus on rate-limiting logic.
A custom storage adapter should focus on persistence and backend communication.
Keeping these responsibilities separate preserves Limify’s architecture and makes the system easier to evolve.