Sitemap

Stop sharing one database between your control plane and runtime

7 min readMay 16, 2026

An architectural split most SaaS teams put off — until it’s painful to add.

Press enter or click to view image in full size

Most SaaS architectures start with a single PostgreSQL instance, an ORM, and good intentions. Configuration lives next to user data, which lives next to runtime state. It’s pragmatic, it’s fast, and for the first eighteen months it’s probably the right call.

Then a customer reports their dashboard is slow during peak hours. You investigate and find a noisy background job — running on the same database — locking rows your read queries need. You add an index. Two months later, an outage in your billing service brings down inference. Six months later, your team is paged at 2am because a schema migration on a config table held a lock long enough to time out an unrelated workflow.

This is the conversation about when to split your database into a control plane and a runtime plane — and from where I’m sitting, the answer is: earlier than you think, and almost always before you feel pressure to do it.

What is “control plane” vs “runtime”?

Borrowing the term from networking, the control plane is where things are configured, managed, and observed:

  • Organization, user, role, and permission tables
  • Agent definitions, campaign settings, billing data
  • Schedule configuration, feature flags, integration secrets
  • Audit logs, exported analytics, settled call history

The runtime plane is where work actually happens:

  • Live workflow state (active conversations, in-flight executions)
  • Per-execution context (turn-by-turn agent state)
  • Real-time telemetry, latency-sensitive lookups
  • Ephemeral data with short retention windows

For a voice AI platform: when an operator changes the prompt for an agent in the dashboard, that’s the control plane. When that agent is mid-call, generating turns and tracking conversational state — that’s the runtime.

Most teams put both in the same database because, honestly, they look similar. They’re both rows. They both need PostgreSQL. They both have foreign keys. The seams aren’t visible until they start tearing.

Five forces pushing them apart

1. Access patterns differ in kind, not degree

Control plane traffic is bursty and read-skewed: humans loading dashboards, occasional writes when something is configured. Runtime traffic is high-throughput and write-heavy: every turn of a conversation hits the database.

These workloads want different things. The control plane wants strong consistency, generous query plans, and low jitter for human-facing reads. The runtime wants fast appends, predictable latency under load, and write amplification minimized.

Tuning a single database to satisfy both is an exercise in compromise. Splitting them lets each be tuned independently — different connection pool sizes, different replica strategies, different fsync configurations.

2. They scale on different axes

Control-plane growth is roughly linear with the number of paying organizations. A SaaS with 1,000 orgs has a manageable control plane.

Runtime growth is multiplicative. The same 1,000 orgs running 10,000 concurrent executions produces a workload three orders of magnitude larger. If they share a database, your control plane’s read replicas and your runtime’s write throughput compete for the same cluster.

Split, the runtime can move to partitioned tables, different storage backends, or even a non-relational store. The control plane stays simple, normalized, and queryable.

3. Blast radius matters

A runtime database failure should not bring down org configuration. An expensive control-plane query (someone exporting six months of call records) should not slow down active calls.

Sharing a database means sharing failure modes. A long-running migration on a config table can lock just enough that runtime writes pile up. A poorly-indexed audit query can saturate IOPS during business hours.

A split forces you to design with graceful degradation in mind. If the control plane is unreachable, in-flight workflows continue with cached config. If the runtime is unreachable, the dashboard still works — operators can still investigate and configure. At enterprise scale, this isn’t a luxury; it’s a baseline.

4. Schema cadence is wildly different

Control-plane tables change with the product roadmap. New features bring new tables, new columns, new indexes. The schema is a living artifact, edited weekly.

Runtime tables, once stable, change rarely. The shape of “an active conversation” or “a turn” tends to harden after the first year. When it changes, it changes carefully — wrong moves disrupt live workflows.

Sharing a database means every product migration risks rippling into runtime stability. Splitting means runtime schema is a different concern with a different cadence and a stricter review process.

5. Compliance and retention diverge

Control-plane data — user records, audit trails, billing — has long retention requirements and strict access controls. It’s the system of record for “who did what.”

Runtime data is often ephemeral: conversational state from yesterday isn’t usually retained. In regulated industries (healthcare, finance), runtime data may have shorter legal retention than control plane — keeping conversational PII longer than necessary is a liability.

Two databases mean two retention policies, two backup strategies, two encryption-at-rest configurations. One database means everything inherits the strictest policy, which is operationally expensive and legally risky if you ever need to prove a runtime row was actually purged.

What lives where

A simplified split for a workflow-style platform:

Lives in control planeLives in runtimeOrganizations, users, rolesIn-flight workflow statePermissions, RBAC policiesTurn-by-turn agent contextAgent / workflow configurationLive telemetry buffersCampaign definitionsActive session tokensBilling, subscription, quotasPer-execution ephemeral logsAudit logs, exported reportsReal-time queue stateIntegration secrets (encrypted)Cache-tier hot lookupsFeature flagsSettled call history (index)

The boundary isn’t always obvious. Call history is interesting — the index of what happened lives in the control plane (operators query it from dashboards), but the raw turn-by-turn detail might live in the runtime or in object storage. The right answer depends on retention and access patterns.

The patterns that make the split work

Three patterns we relied on:

Cross-database references are IDs, not foreign keys. A runtime row referring to an organization stores org_id as an opaque UUID. No foreign key constraint. The runtime trusts the control plane to be the source of truth for what org_id means.

Sync is event-driven, not transactional. When a configuration changes in the control plane, an event is published. The runtime subscribes and updates its in-memory cache. No two-phase commit. Eventual consistency is acceptable because configuration changes are rare and the runtime can re-read on miss.

Joins happen in the application layer, not the database. When the dashboard needs to display “calls per organization,” the application queries both planes and stitches results. This sounds expensive but it’s almost never the bottleneck — and it forces explicit thinking about which queries are cheap and which aren’t.

The counter-arguments, addressed

“Operational complexity: now I have two databases.” True. You also have two opportunities to scale, two blast-radius boundaries, and two retention policies. The marginal operational cost of a second managed PostgreSQL instance is much lower than the cost of a runtime outage caused by a config migration.

“I’ll lose cross-database transactions.” You won’t have them. You’ll have to rethink any flow that depended on them. This is a feature, not a bug — those flows were probably wrong anyway. Distributed systems don’t have free transactions.

“It’s too expensive to run two instances.” Two right-sized instances often cost less than one over-provisioned one. The runtime might want NVMe and high IOPS; the control plane is happy on standard storage. Picking the right tier per workload usually saves money.

“It’s premature optimization.” It’s not optimization. It’s an architectural seam, and seams are cheaper to introduce when the codebase is small. Splitting databases at year four — when six services and two hundred migrations are entangled — is the kind of work that takes a quarter and produces no new features.

When not to split

Skip this pattern if you’re:

  • Pre-product-market-fit with no real customers
  • Single-tenant on-premise software where blast radius is moot
  • Operating a simple CRUD app where “control plane” and “runtime” are the same thing
  • A team of two without the operational maturity to run two databases

In those cases, the single-database default is correct. The split makes sense when you have meaningful concurrent traffic, multiple tenants, and a runtime that runs continuously rather than on-demand.

The framing that helps

Think of it this way: you don’t have a database. You have two components that happen to use databases. The control plane component manages configuration, identity, billing, and audit. The runtime component executes work. They communicate through events and stable APIs. The fact that each persists to PostgreSQL is an implementation detail.

When you frame it that way, the question stops being “should we split the database?” and becomes “are these the same component?” Almost always, the answer is no.

The team that splits early thanks themselves later. The team that splits at year four wishes they’d done it at year one.

I’m a senior software engineer with experience in multi-tenant control planes for AI voice-agent platforms. I've built infrastructure that handles tens of thousands of calls per day in regulated industries (healthcare, finance, insurance). Available for senior freelance engagements — find me on LinkedIn or GitHub.

--

--

Matheus Lúcio
Matheus Lúcio

Written by Matheus Lúcio

Passionate Software Engineer with 12+ years' experience, skilled in Node.js, and React. Expert in scalable, innovative tech solutions. https://mathz.dev