Skip to content

Redis Integration

Redis is an optional but recommended addition for production deployments. When KIRAK_REDIS_URL is set, three database-backed auth utilities are transparently upgraded to Redis equivalents.

Install:

Terminal window
pip install "kirak[redis]"

KIRAK_REDIS_URL=redis://localhost:6379

URL formats:

# No authentication
KIRAK_REDIS_URL=redis://localhost:6379
# With password
KIRAK_REDIS_URL=redis://:your-password@localhost:6379
# TLS (managed Redis -- Upstash, Redis Cloud, ElastiCache)
KIRAK_REDIS_URL=rediss://your-endpoint:6380
# With user + password (Redis 6+ ACLs)
KIRAK_REDIS_URL=redis://username:password@localhost:6379

For a scheduler Redis backend that uses TLS on a redis:// URL (unusual but some providers), force TLS with the provider’s redis_ssl setting in kirak.json:

{ "scheduler": { "providers": { "redis": { "type": "redis", "redis_ssl": true } } } }

Feature Without Redis With Redis
Rate limiting (CRUD and auth endpoints alike) DB fixed-window (kirak_rate_limits table) Redis sliding-window sorted set – more accurate, auto-expires
Token blacklist DB row (auth_token_blacklist table) Redis key with per-token TTL – automatically cleaned up
OTP storage DB row (auth_otp table) Redis key with OTP TTL – automatically cleaned up
API key lookup DB query on every request Cached in Redis for 60 seconds
CRUD query caching (fetch/search/count/exists, REST and GraphQL field queries) No caching – every read hits the DB TTL-bound query cache, per-model configurable

The first four switch transparently just by setting KIRAK_REDIS_URL – no code changes, no migration needed. The kirak_rate_limits, auth_token_blacklist, and auth_otp tables remain in the schema but are unused while Redis is active. The 60-second API key cache means a revoked key can still be accepted for up to 60 seconds after revocation – see API Keys.

Query caching is different: it needs Redis configured and an explicit opt-in (kirak.json’s cache.enabled, or a per-model "cache" block) – unlike the other four, it doesn’t turn on just because KIRAK_REDIS_URL is set. See Query Caching below.

The Redis client also pings the server every 30 seconds and auto-reconnects on failure – it’s more than pure lazy connection setup.


With Redis, both the hardcoded auth endpoint limits and per-model CRUD limits use a sliding window algorithm instead of a fixed window, enforced atomically via a Lua script:

  • Fixed window (DB): counts requests in fixed time buckets. A burst at the bucket boundary (e.g. 5 at 11:59 + 5 at 12:00) passes as two separate windows.
  • Sliding window (Redis): counts requests in a rolling window ending at “now”. The burst above is seen as 10 requests in the window and is correctly blocked.

Rate limit keys in Redis follow: kirak:rl:{key}, e.g. kirak:rl:login:ip:192.168.1.1 for the hardcoded auth limits, or kirak:rl:crud:posts:fetch:ip:192.168.1.1 for a per-model CRUD limit. See Rate Limiting for the full key format per scope, global defaults, per-operation config, and the admin API for inspecting/resetting individual counters.


CRUD reads (fetch, search, count, exists – REST routes, script mode, and GraphQL field queries all share the same underlying functions) can be cached in Redis, keyed so two users entitled to see different rows never share a cache entry, and automatically invalidated whenever the model they read from is mutated.

Enable it in kirak.json for a framework-wide default:

{ "cache": { "enabled": true, "default_ttl_seconds": 60 } }

Or per-model in models.json, sibling to rate_limit:

"cache": {
"enabled": true,
"ttl_seconds": 60
}

A model without a "cache" block inherits the kirak.json default. "enabled": false is an explicit, always-boolean opt-out (never an empty {} sentinel).

Cache keys follow a generational scheme, not per-row invalidation:

kirak:cache:gen:{model_name} -> integer, INCR'd on invalidation
kirak:cache:q:{model_name}:{generation}:{sha256(sql+params)} -> cached response, TTL-bound

Since row-level security is baked directly into the SQL string before it reaches Redis, two different users querying the same model with the same filters get different cache keys automatically whenever their RLS scope differs – there’s no separate “scope by user” logic to get wrong.

A create/update/delete/upsert/destroy/restore on a model INCRs that model’s generation counter, which makes every previously-cached entry for it unreachable (old entries just expire on their own TTL rather than being actively deleted) – an O(1) operation with no SCAN/KEYS pattern-delete, which is the usual way naive Redis cache invalidation becomes an incident on a busy keyspace.

GraphQL mutations get one extra step: a mutation document can carry several root fields sharing one transaction, so a write isn’t durable until the whole document commits. Rather than invalidating too early (which could let a concurrent read cache stale data under the new generation, where nothing would catch it again until the TTL expires), GraphQL mutations queue their invalidations and flush them once, immediately after the transaction actually commits – and never flush at all if the transaction rolls back.

Without Redis, or with caching disabled: every read goes straight to the DB, exactly as before – no code changes required in an application either way.


When logout is called, the token’s jti is added to Redis with the key kirak:bl:{jti} and a TTL equal to the token’s remaining lifetime. When the token expires, the Redis key expires automatically – no cleanup job needed.

Without Redis, blacklist entries accumulate in the auth_token_blacklist table and must be periodically purged.


OTP codes are stored as kirak:otp:{phone_number} with a short TTL. When the TTL expires, the OTP is automatically invalidated. No background job or explicit deletion is required.


Redis utilities use fail-open semantics – if Redis becomes unavailable, the request is allowed through – except the token blacklist, which fails closed.

Specifically:

  • Rate limiting: if the Redis check fails, the request is allowed (no limit enforced).
  • Token blacklist: if the Redis check fails, the token is refused with 503 AUTH_UNAVAILABLE, and a logout that cannot write the blacklist fails with the same code – a logged-out token never works.
  • OTP: if the Redis check fails, the OTP is considered valid (best-effort).

The trade-off: during a Redis outage rate limits are temporarily unenforced, and authenticated requests fail with 503 until Redis is back (requests without a token, and the public auth routes, are unaffected).


Redis is also used as the Scheduler queue backend (separate from auth Redis). The provider choice is a kirak.json setting, not an environment variable:

{
"scheduler": {
"default_provider": "redis",
"providers": { "redis": { "type": "redis" } }
}
}
KIRAK_SCHEDULER_REDIS_URL=redis://localhost:6379

The scheduler Redis backend uses list-based queues and is independent of the auth Redis integration. Both can point to the same Redis instance or separate ones.


Kirak does not currently validate the Redis connection at startup – it initializes lazily on first use. If you want to assert Redis availability at boot:

def on_kirak_ready(kirak):
import os
redis_url = os.getenv("KIRAK_REDIS_URL", "")
if redis_url:
import asyncio
import redis.asyncio as aioredis
async def check():
r = await aioredis.from_url(redis_url)
await r.ping()
await r.aclose()
asyncio.get_event_loop().run_until_complete(check())
print("Redis connection verified")