Cloud Architecture·8 min read·

Control Plane vs Data Plane for a Small SaaS

Split admin, billing, and provisioning from request serving on one VPS so a hung control action cannot stall the path your customers actually hit.

NB

Netbay Cloud Team

Netbay Engineering

On this page

A small SaaS on one VPS still has two planes, even if both planes are systemd units on the same kernel. The data plane is the path a customer uses every minute: login, API reads, page renders, webhooks you must acknowledge quickly. The control plane is everything that changes the product: signups, plan changes, admin impersonation, schema migrations, and the scripts that create resources. When those share one process, a slow billing call blocks a checkout, and a stuck admin export blocks every GET. Split them by listen port, by unit, and by timeout before you split them by machine.

Two planes, one box, different promises

The data plane promise is latency and availability. It should do almost no writes that can wait, and it should time out fast. The control plane promise is correctness. It may take two seconds to apply a plan change. It may lock a row. It may call a provider API. Those two promises fight inside a single event loop. The fix on one VPS is two processes: app-data.service on 127.0.0.1:3000 and app-control.service on 127.0.0.1:3001. Nginx routes /api/ and / to the data port, and /admin/ plus /billing/ to the control port. Both may use the same Postgres. They must not share a request queue.

This is the same idea as a cloud provider's split between "provision a server" and "serve packets," scaled down. You do not need a fleet. You need a hung POST /admin/export to leave GET /api/items alone.

Control plane and data plane on one VPS Client browser / API nginx :443 path-based split DATA PLANE :3000 GET/POST app CONTROL PLANE :3001 admin/billing Postgres shared rows, separate queues same disk, same VPS, different units and timeouts

Route at nginx, isolate in systemd

Path routing is enough. Do not invent a second hostname until you need one. Keep TLS at nginx. Give the control unit a higher TimeoutStopSec (it may be in a transaction) and a lower MemoryMax if admin exports are the leaky ones. Give the data unit the opposite: short stop, more RAM, more workers.

nginx
# /etc/nginx/sites-available/saas
upstream data_plane {
    server 127.0.0.1:3000;
    keepalive 16;
}
upstream control_plane {
    server 127.0.0.1:3001;
    keepalive 4;
}
server {
    listen 443 ssl http2;
    server_name app.example.in;
    location /admin/ { proxy_pass http://control_plane; }
    location /billing/ { proxy_pass http://control_plane; }
    location / {
        proxy_pass http://data_plane;
        proxy_read_timeout 10s;
    }
}

The 10s read timeout on the data plane is a feature. A control handler that needs 30s to talk to a provider should not be reachable under /api/. Put it under /billing/ with a longer timeout on that location only. Customers who never visit /admin/ should never wait on it.

Shared database, split workloads

Both planes can use one Postgres on 127.0.0.1. Give them different roles: app_data with SELECT/INSERT on customer tables, app_control with the extra rights for plan updates and admin reads. Connection limits per role stop a control-plane leak from taking every slot the data plane needs. Migrations are a control-plane job: stop or drain control, run the oneshot unit, then bounce data if the schema requires new code. Do not auto-migrate on data-plane boot; that is how two workers race a lock during a deploy.

ini
# /etc/systemd/system/app-data.service
[Service]
User=app
EnvironmentFile=/etc/app/env-data
ExecStart=/usr/bin/node dist/data.js
Restart=on-failure
TimeoutStopSec=15
MemoryMax=1G

# /etc/systemd/system/app-control.service
[Service]
User=app
EnvironmentFile=/etc/app/env-control
ExecStart=/usr/bin/node dist/control.js
Restart=on-failure
TimeoutStopSec=40
MemoryMax=512M

Health checks should name the plane. /healthz on the data port checks Postgres select 1 and Redis ping. /healthz on the control port checks the same plus "can I reach the provider API." An uptime robot should hit the data plane. Paging on control-plane health is optional; paging on data-plane health is not.

When the product grows, the first machine you add is often a second VPS for the database or a replica, not a cluster product. Netbay does not sell a managed cluster, and you do not need one to keep planes apart. Two units, two ports, one Lucknow DC01 instance is a control plane and a data plane that you can explain on a whiteboard.

Rate limits belong to the plane, not to the process. A burst of admin CSV exports should 429 on /admin/ and leave /api/ alone. Nginx limit_req_zone keyed on $binary_remote_addr, with a tighter zone on the control locations, is enough. Do not share one zone across both planes or a hostile admin session starves customers. Logs should tag the plane too: add X-Plane: data or X-Plane: control at nginx so journald and access logs are greppable when an incident starts with "the site is slow" and you need to know which queue filled first.

Spin up Ubuntu 24.04 on Netbay, split the two units, and keep customer traffic off the admin queue — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance