← All insightsSecurity

Millisecond authorization: caching permissions safely

Every request checks permissions, and a database round trip per check is slow. Serving permission rows from memory kept in sync makes the check fast; here are the safety questions and honest answers.

4 min read

Every request an application handles begins with the same question: is this caller allowed to do this? The answer lives in a handful of tables: users, roles, the mapping between them, and the permissions (sometimes called scopes) each role carries. Checking those tables against the database on every call is the most repeated read in the system, and on a busy service it is the read that sets the floor for latency.

Why the database is the wrong place for the constant check

A permission check is small, but it is on the path of everything. A page that makes ten calls to the backend makes ten permission checks, and each one is a round trip to the database before any useful work starts. At a few milliseconds each that is the difference between a fast page and a sluggish one, and the p99 (the latency the slowest one percent of requests see) is the number that suffers most, because those are the requests that queue behind a busy database.

The load side is worse. Permission tables change rarely, so the database is answering the same question millions of times a day with the same answer. That is capacity spent on nothing.

Why the hand-built cache is also wrong

Most teams know this and build an in-memory permission cache inside the application. It works until an administrator revokes a role and the user keeps their access for as long as the cache entry lives. Expiry timers are the usual fix, and they force a choice: a short timer means the database is hit often anyway, and a long timer means a revoked permission lives on for minutes.

There is also the many-servers problem. A cache inside each application server is a separate copy per server, and revoking a permission means clearing all of them. Teams either broadcast clear-the-cache messages, which is a sizeable project of its own, or accept that some servers will be stale.

The permission cache is the one cache where being stale is not a performance problem but a security one.

Serving permission rows from a synced memory store

redfly keeps the user, role and permission tables in Redis (a memory store shared by every application server, answering in about a millisecond) and serves reads from there through its API. A sync service watches the database for changes to those tables. On SQL Server it reads change tracking on a short interval; on PostgreSQL and MongoDB it streams the changes through logical replication and change streams. A changed row reaches Redis shortly after.

The application calls Get or GetRows for the caller's roles and scopes and gets a memory read. There is one shared copy, so there is nothing to broadcast. Joins are served from Redis too, so the user-to-role-to-permission lookup is one call, not three. And there are no expiry timers in the application, because the copy is refreshed by the change itself.

The safety questions, answered honestly

How fast does a revoked permission disappear? Shortly after the row changes in the database; on SQL Server the interval at which change tracking is read sets the bound, and on the streamed engines it is the small delay in the stream. That is not instant. For a session that must end the moment an administrator clicks, the application should also check a short-lived token (a signed pass that expires quickly) or make a database read at the sensitive operation, and we say so rather than promise otherwise.

What if the cache is unreachable? Reads fall back to the database automatically. The permission check gets slower, not wrong; it reads the same tables it would have read before the cache existed. There is no mode where the check is skipped.

Can the cache grant a permission the database never did? Only by being stale in the other direction: a newly granted role takes a moment to appear. That is the safe direction, and it is the one to design for.

What about the credentials themselves? Password hashes (the scrambled form passwords are stored in) and other secrets should not be in any cache, and the tables synced to Redis are chosen by the customer; the sync touches only what its database connection is authorized to read. Keep the secret tables out of the sync and check them against the database.

Where the lines are

A short lag is right for authorization on ordinary requests: viewing a page, listing rows, calling a service. It is not right for the action that moves money or deletes data, where the application should confirm against the database inside the same transaction. The cache takes the millions of ordinary checks off the database so that the rare expensive check has a database with headroom to answer it.

The lag itself should be measured, not assumed. Measure the read interval on SQL Server, or the stream delay on the other engines, and write the number into the security review so the people who own the permission model know what they are accepting.

Where redfly fits

redfly supplies the API and the sync service for the customer's own SQL Server, MongoDB or PostgreSQL database, deployed in the customer's cloud account or on their own servers. The permission model, the tables and the decision about which checks go to the database stay with the customer's team.

Ready when you are

Stop reading. Start shipping.

Work with us as a design partner and see the difference on your own database.

redfly API + Sync Service · Licensed directly from redfly