Skip to main content
Updated Feb 23, 2026

Namespaced Actors for Multi-Tenancy

Your TaskActor works brilliantly for a single customer. But now you're building a SaaS platform where hundreds of companies use your AI task management system. Acme Corporation and Globex Industries both use TaskActors, but their data must never mix.

The nightmare scenario:

Customer: Acme Corporation
Request: GET /actors/TaskActor/task-123

Expected: Acme's task-123 (Q4 budget review)
Actual: Globex's task-123 (Confidential merger plan)

Result: Data breach, lawsuit, destroyed reputation

This isn't hypothetical. Multi-tenant data leaks have cost companies billions in settlements, lost customers, and regulatory fines. The 2021 Microsoft Power Apps breach exposed 38 million records across multiple organizations because of misconfigured tenant isolation.

You could solve this with careful actor ID naming (acme-task-123, globex-task-123), but that's fragile. A bug in your ID generation logic becomes a data breach. What you need is architectural isolation: a design where cross-tenant access is impossible by construction, not convention.

Dapr's namespaced actors provide exactly this.

What Are Namespaced Actors?

Namespaced actors deploy the same actor type into different Kubernetes namespaces, each with isolated state. Dapr's official documentation states it clearly:

"With actor namespacing, the same actor type can be deployed into different namespaces. You can call instances of these actors in the same namespace."

The key insight: Kubernetes namespace + separate state store = complete tenant isolation.

                    NAMESPACED ACTORS ARCHITECTURE
==============================

┌─────────────────────────────────────────────────────────────┐
│ KUBERNETES CLUSTER │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ NAMESPACE: tenant-a │ │ NAMESPACE: tenant-b │ │
│ │ │ │ │ │
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
│ │ │ TaskActor │ │ │ │ TaskActor │ │ │
│ │ │ (tenant-a copy) │ │ │ │ (tenant-b copy) │ │ │
│ │ └────────┬─────────┘ │ │ └────────┬─────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
│ │ │ State Store │ │ │ │ State Store │ │ │
│ │ │ (Redis DB 1) │ │ │ │ (Redis DB 2) │ │ │
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │
│ │ │ │ │ │
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
│ │ │ Dapr Sidecar │ │ │ │ Dapr Sidecar │ │ │
│ │ │ (app-id: task) │ │ │ │ (app-id: task) │ │ │
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │
│ │ │ │ │ │
│ │ CANNOT access │ │ CANNOT access │ │
│ │ tenant-b actors ✗ │ │ tenant-a actors ✗ │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ DAPR CONTROL PLANE │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ Placement Service │ │ │
│ │ │ - Tracks actors by namespace │ │ │
│ │ │ - tenant-a sidecars get ONLY tenant-a actors │ │ │
│ │ │ - tenant-b sidecars get ONLY tenant-b actors │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

The Isolation Guarantee

Namespaced actors enforce isolation at three levels:

Isolation LayerWhat It Prevents
Namespace boundaryApps in namespace-a cannot invoke actors in namespace-b
Placement serviceSidecars only receive actor locations for their own namespace
State storeEach namespace uses a separate state store (or separate database within shared infrastructure)

This is defense in depth. Even if application code has a bug that tries to access another tenant's actor, the infrastructure blocks it.

Configuring Namespace Isolation

Step 1: Create Kubernetes Namespaces

# Create namespaces for two tenants
kubectl create namespace tenant-acme
kubectl create namespace tenant-globex

# Verify namespaces
kubectl get namespaces | grep tenant

Output:

tenant-acme    Active   5s
tenant-globex Active 5s

Step 2: Configure Separate State Stores

Each namespace needs its own state store component. You can use:

  • Separate Redis instances (highest isolation)
  • Same Redis instance with different databases (practical for most cases)
  • Same database with key prefixes (minimum isolation, not recommended for actors)

Here's the recommended approach using Redis database isolation:

# components/tenant-acme/statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: tenant-acme
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: redis-master.redis.svc.cluster.local:6379
- name: redisPassword
secretKeyRef:
name: redis-secret
key: password
- name: actorStateStore
value: "true"
- name: redisDB
value: "1" # Tenant Acme uses Redis DB 1
# components/tenant-globex/statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: tenant-globex
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: redis-master.redis.svc.cluster.local:6379
- name: redisPassword
secretKeyRef:
name: redis-secret
key: password
- name: actorStateStore
value: "true"
- name: redisDB
value: "2" # Tenant Globex uses Redis DB 2

Critical configuration: The actorStateStore: "true" metadata is required for any state store used by actors. Without it, actors cannot persist state.

Apply the components:

kubectl apply -f components/tenant-acme/statestore.yaml
kubectl apply -f components/tenant-globex/statestore.yaml

Step 3: Deploy Actors to Each Namespace

Deploy the same TaskActor service to both namespaces:

# k8s/tenant-acme/task-actor-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: task-actor-service
namespace: tenant-acme
spec:
replicas: 2
selector:
matchLabels:
app: task-actor
template:
metadata:
labels:
app: task-actor
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "task-actor"
dapr.io/app-port: "8000"
dapr.io/enable-actors: "true"
spec:
containers:
- name: task-actor
image: your-registry/task-actor:v1.0
ports:
- containerPort: 8000
# k8s/tenant-globex/task-actor-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: task-actor-service
namespace: tenant-globex # Only difference: namespace
spec:
replicas: 2
selector:
matchLabels:
app: task-actor
template:
metadata:
labels:
app: task-actor
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "task-actor" # Same app-id in both namespaces
dapr.io/app-port: "8000"
dapr.io/enable-actors: "true"
spec:
containers:
- name: task-actor
image: your-registry/task-actor:v1.0
ports:
- containerPort: 8000

Notice that app-id is the same in both namespaces. This is intentional. The namespace provides the isolation, not the app-id.

kubectl apply -f k8s/tenant-acme/task-actor-deployment.yaml
kubectl apply -f k8s/tenant-globex/task-actor-deployment.yaml

How Placement Service Enforces Isolation

The Dapr Placement service is the key to namespace isolation for actors. Here's what happens:

             PLACEMENT SERVICE ISOLATION
===========================

Sidecar in tenant-acme Placement Service
┌─────────────────────┐ ┌─────────────────────┐
│ "Where is │ │ │
│ TaskActor/task-1 │──Request──>│ Check namespace: │
│ in MY namespace?" │ │ tenant-acme │
└─────────────────────┘ │ │
│ Return actors │
┌─────────────────────┐ │ ONLY in │
│ Response: │<──Response──│ tenant-acme │
│ TaskActor/task-1 │ │ │
│ is on pod-xyz │ │ NEVER reveal │
└─────────────────────┘ │ tenant-globex │
│ actor locations │
└─────────────────────┘

The Placement service maintains separate actor registries per namespace. Sidecars in tenant-acme only receive placement information for actors in tenant-acme. They literally cannot discover actors in other namespaces.

Cross-Namespace Communication: Blocked by Design

What happens if code in tenant-acme tries to invoke an actor in tenant-globex?

# Code running in tenant-acme namespace
from dapr.actor import ActorProxy, ActorId

# Attempt to access actor in another namespace
proxy = ActorProxy.create(
"TaskActor",
ActorId("task-123"),
TaskActorInterface
)

# This will FAIL - no route to tenant-globex actors
result = await proxy.get_task() # Error: Actor not found

The invocation fails because:

  1. The sidecar asks Placement: "Where is TaskActor/task-123?"
  2. Placement only knows about actors in tenant-acme
  3. If the actor doesn't exist in tenant-acme, it returns "not found"
  4. Even if an actor with the same ID exists in tenant-globex, it's invisible

This is the isolation guarantee: actors in different namespaces are mutually invisible.

State Store Isolation Patterns

PatternIsolation LevelUse When
Separate Redis instancesMaximumRegulatory requirements (HIPAA, SOC2), large tenants
Same Redis, different DBsHighMost SaaS use cases, good balance of isolation and efficiency
Same Redis, key prefixesMinimumNOT recommended for actors (Dapr doesn't prefix automatically)
etcd with key prefixesMediumWhen using etcd and DB separation isn't available

Redis supports 16 logical databases (0-15 by default). Each database is isolated:

Redis Instance
┌────────────────────────────────────────────┐
│ │
│ DB 0: Default (don't use) │
│ │
│ DB 1: tenant-acme │
│ ┌──────────────────────────────────────┐ │
│ │ TaskActor||task-1 -> {...} │ │
│ │ TaskActor||task-2 -> {...} │ │
│ └──────────────────────────────────────┘ │
│ │
│ DB 2: tenant-globex │
│ ┌──────────────────────────────────────┐ │
│ │ TaskActor||task-1 -> {...} │ │
│ │ TaskActor||task-3 -> {...} │ │
│ └──────────────────────────────────────┘ │
│ │
│ DB 3-15: Available for more tenants │
│ │
└────────────────────────────────────────────┘

Notice that both tenants can have task-1. They're completely separate in different Redis databases.

etcd with Prefix Isolation

If you're using etcd, configure keyPrefixPath:

# components/tenant-acme/statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: tenant-acme
spec:
type: state.etcd
version: v1
metadata:
- name: endpoints
value: etcd.etcd.svc.cluster.local:2379
- name: actorStateStore
value: "true"
- name: keyPrefixPath
value: "/tenants/acme/" # All keys prefixed with this path

SQLite with Table Isolation

For SQLite (useful in development or edge deployments):

# components/tenant-acme/statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: tenant-acme
spec:
type: state.sqlite
version: v1
metadata:
- name: connectionString
value: "/data/actors.db"
- name: actorStateStore
value: "true"
- name: tableName
value: "tenant_acme_actors" # Separate table per tenant

Self-Hosted Mode: Environment Variables

For local development or self-hosted Dapr, set the namespace via environment variable:

# Terminal 1: Run tenant-acme actor service
export NAMESPACE=tenant-acme
dapr run --app-id task-actor --app-port 8000 -- python main.py

# Terminal 2: Run tenant-globex actor service
export NAMESPACE=tenant-globex
dapr run --app-id task-actor --app-port 8001 -- python main.py

Each instance uses its own namespace for actor isolation.

Security Considerations

Namespace isolation protects against accidental cross-tenant access, but additional measures harden security:

ThreatMitigation
Malicious actor codeNamespace RBAC prevents accessing other namespace resources
Network sniffingmTLS encrypts all inter-sidecar communication
State store accessSeparate credentials per namespace state store
Placement service compromiseRun Placement in isolated namespace with minimal access

RBAC for Namespace Isolation

# rbac/tenant-acme-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: tenant-acme
name: tenant-actor-role
rules:
- apiGroups: ["dapr.io"]
resources: ["components"]
verbs: ["get", "list"]
# Can only access components in tenant-acme

Reflect on Your Skill

You extended your dapr-deployment skill to include actor patterns. Does it now cover multi-tenant deployment scenarios?

Test Your Skill

Using my dapr-deployment skill, design a multi-tenant TaskActor deployment
for a SaaS platform with 50 customers. Each customer needs complete state
isolation. What's the most efficient approach?

Does your skill recommend:

  • Kubernetes namespace per tenant?
  • Separate state stores (or Redis DBs)?
  • Why Placement service isolation is critical?

Identify Gaps

Ask yourself:

  • Did my skill explain WHY separate state stores are required?
  • Did it mention the Placement service's role in isolation?
  • Did it cover what happens if code tries cross-namespace access?

Improve Your Skill

If you found gaps:

My dapr-deployment skill covers basic namespacing but not the security
rationale. Update it to include:
- Why each namespace needs its own state store (not just app isolation)
- How Placement service enforces namespace boundaries
- What fails when cross-namespace actor access is attempted
- Best practices for Redis DB isolation vs separate instances
- When to use separate Redis instances (compliance requirements)

Try With AI

Open your AI companion (Claude, ChatGPT, Gemini) and explore these scenarios.

Prompt 1: Design Multi-Tenant Architecture

I'm building a SaaS task management platform. Each customer (tenant) should
have completely isolated TaskActors. I expect 100 tenants initially, growing
to 1000.

Help me design the namespace and state store strategy:
- Should each tenant get a Kubernetes namespace?
- How do I handle state store isolation with Redis?
- What happens when I scale to 1000 tenants (Redis only has 16 DBs)?
- What's the operational overhead of managing 1000 namespaces?

Don't give me code yet; I want to understand the architectural trade-offs
first.

What you're learning: How to scale multi-tenant actor systems. The AI helps you understand when namespace-per-tenant works and when you need alternative partitioning strategies (like dedicated Redis instances per tenant group).

Prompt 2: Verify Isolation Works

I've deployed namespaced actors to tenant-acme and tenant-globex. How do I
verify the isolation actually works?

Help me create a test plan:
1. How do I confirm actors can't discover each other across namespaces?
2. How do I verify state stores are truly separate?
3. What logs or metrics show isolation is enforced?
4. How would I detect if isolation accidentally breaks?

Show me specific kubectl commands and API calls to test this.

What you're learning: Validation strategies for multi-tenant isolation. The AI helps you build confidence that your security boundaries work before going to production.

Prompt 3: Handle Compliance Requirements

My healthcare SaaS platform needs HIPAA compliance. Each hospital tenant
must have data that never touches another tenant's data, even in memory.

Current plan: Kubernetes namespace per tenant with Redis DB isolation.

Questions:
- Is Redis DB isolation sufficient for HIPAA, or do I need separate instances?
- What about the Placement service - does it have any shared state?
- How do I audit cross-tenant access attempts?
- What documentation do I need for compliance auditors?

Help me understand the compliance implications of my architecture choices.

What you're learning: How regulatory requirements affect actor deployment. The AI helps you understand when logical isolation is sufficient and when physical separation is required.

Safety Note

Multi-tenant isolation failures can expose sensitive customer data. Always:

  1. Test isolation in staging before production
  2. Use separate state stores, not just namespace separation
  3. Enable audit logging for actor invocations
  4. Conduct periodic penetration testing of tenant boundaries
  5. Have a data breach response plan even with strong isolation

AI suggestions for isolation patterns should be validated against your specific compliance requirements (HIPAA, SOC2, GDPR, etc.) and reviewed by your security team.