Most enterprise AI platforms reach production with their security stories told at the application layer. Query filters live in ORM middleware, tenant scoping is a convention enforced by developer discipline, and Redis is treated as a fast key-value store without much thought for what happens when one tenant's workload saturates the event loop. These are not edge cases. They are the default architecture for a large proportion of multi-tenant SaaS platforms, and they carry failure modes that only become visible under production load or during a security incident.
This article covers three specific infrastructure mistakes: application-layer query filters that leave enforcement gaps, the Postgres row-level security mechanism that closes them, and the Redis blocking patterns that create invisible latency ceilings across your entire tenant fleet.
Why Application-Layer Filters Are Not Enforcement
ORM-level query filters feel authoritative because they are consistent. Every model query passes through the same middleware, and in normal operation, tenant scoping works exactly as expected. The problem is that the enforcement boundary is the application process, not the database.
Raw SQL executed through a database connection bypasses ORM middleware entirely. Any developer writing a performance-optimised query, any data migration script, or any background job that drops to the connection layer operates outside the filter chain. The filter was never enforced at the layer where data actually lives.
Attached entity traversal compounds this. When an ORM loads a related object by following a foreign key, the scoping logic often does not propagate to the join. A correctly scoped parent query can return child records belonging to a different tenant if the relationship is not explicitly filtered at every level. This is not a hypothetical: it is a class of bug that appears regularly in code review and almost never in unit tests.
Postgres Row-Level Security as the Enforcement Layer
Row-level security (RLS) in Postgres moves tenant isolation from the application process into the database engine itself. Policies are defined at the table level and evaluated by the query planner on every read and write, regardless of how the query arrived. A raw SQL connection, a migration script, and an ORM query are all subject to the same policy.
Policy Design
The standard pattern sets a session variable on connection establishment, typically SET app.current_tenant_id = '...', and references that variable in the policy expression. Every query against a tenant-scoped table then implicitly carries a WHERE tenant_id = current_setting('app.current_tenant_id') that cannot be omitted by the caller.
Superuser connections bypass RLS by default, which means database migration tooling needs to be treated as a privileged operation with its own access controls. The policy also needs to cover both SELECT and DML operations explicitly. A policy that only restricts reads will allow cross-tenant writes from an incorrectly scoped mutation.
Performance Considerations
RLS adds a predicate to every query, and that predicate needs an index to perform. A composite index on (tenant_id, primary_key) is the standard starting point, but AI platform workloads often involve vector similarity search or large document retrieval where the query planner's interaction with tenant filters requires explicit testing. Validating RLS under representative query patterns before production is not optional.
Redis Blocking and the Head-of-Line Problem
Redis is single-threaded for command execution. This is a deliberate design choice that simplifies consistency guarantees, but it means that any command which takes a long time to complete blocks every other client waiting on the same instance. In a multi-tenant platform, this creates a failure mode where one tenant's operation degrades latency for all other tenants simultaneously.
The KEYS Command
KEYS pattern is the most common source of this problem. It performs a full keyspace scan, and on an instance with millions of keys, it can block the event loop for hundreds of milliseconds. Developers reach for it during debugging or in administrative scripts, and it frequently finds its way into production code paths when a feature needs to enumerate or invalidate a set of tenant keys.
The correct replacement is SCAN, which iterates the keyspace in incremental cursor steps and yields control between iterations. It is slower in aggregate but does not block other clients during execution. For tenant-specific key enumeration, a well-designed key namespace convention combined with SCAN with a prefix match is the production-safe pattern.
Tenant Isolation in Redis
Shared Redis instances carry a second isolation risk beyond blocking: key namespace collisions. Without a strict prefix convention enforced at the client library level, two tenants can overwrite each other's cache entries if key construction logic contains a bug. Separate logical databases (SELECT index) provide namespace separation but share the same event loop and memory. Separate Redis instances per tenant tier provide genuine isolation but at infrastructure cost. The right answer depends on your tenant count and SLA commitments, but the decision needs to be made deliberately rather than inherited from a default configuration.
Audit and Detection Before Remediation
Before remediating these issues in an existing platform, you need visibility into where enforcement gaps actually exist. Postgres provides pg_stat_activity and the log_min_duration_statement parameter for identifying queries that arrive without expected tenant context. Redis provides SLOWLOG and the MONITOR command for identifying blocking operations in development environments.
The goal of the audit phase is to distinguish between queries that are correctly scoped and happen to arrive through a non-ORM path, and queries that are genuinely unscoped. RLS makes this distinction irrelevant by enforcing at the engine level, but the audit still matters for understanding the blast radius of any historical data exposure.
Where Vector Labs Fits
We design and build production data infrastructure for AI platforms where tenant isolation and reliability under load are non-negotiable requirements. In our recruitment AI build, we structured a multi-source data architecture on AWS with clearly defined data boundaries across candidate and employer records, establishing the kind of access separation that prevents cross-tenant exposure at the storage layer. If you are auditing your current platform's isolation model or building a new one, contact us at vector-labs.ai/contacts.
FAQs
Yes, but the cost is manageable with correct indexing. RLS appends a predicate to every qualifying query, and without an index that covers the tenant_id column alongside the columns used in your WHERE clauses, the planner may fall back to sequential scans. A composite index on (tenant_id, primary_key) is the baseline requirement, and you should test RLS policies against your actual query workload before enabling them in production.
Yes. Postgres superusers and table owners bypass RLS by default unless the policy is created with the FORCE option or the table owner explicitly opts in. This means your migration tooling, backup processes, and any administrative connection pool need to be treated as privileged operations with strict access controls. Application service accounts should connect as non-superuser roles with RLS enforced.
SCAN is the correct replacement because it does not block the event loop for the full duration of a keyspace scan. It is not instantaneous: iterating a large keyspace with SCAN takes multiple round trips, and the keyspace can change between iterations. For cache invalidation use cases, a well-structured key namespace with a predictable prefix is a better long-term solution than any full-keyspace enumeration command.
It depends on your tenant tier model and SLA commitments. A shared Redis instance with a strict key namespace convention is operationally simpler and cost-effective for a large number of small tenants. Enterprise tenants with strict latency SLAs or data residency requirements warrant dedicated instances, because logical database separation within a shared instance still shares the event loop and memory. The decision should be explicit and documented, not a default.
Start with Postgres logging. Setting log_min_duration_statement to capture slow queries and enabling log_line_prefix to include the application name and connection role will surface queries arriving from unexpected connection sources. If you have implemented RLS, you can also log policy evaluation failures. For a more systematic audit, compare the connection roles used by your ORM, your migration tooling, and any background jobs against the roles that have RLS enforced.

