Skip to main content

tenement

Lightweight Rust hypervisor for single-server deployments of many single-tenant processes.


tenement is a process hypervisor for running multi-tenant services on a single server. It spawns one process per tenant, routes requests by subdomain, runs HTTP health checks, and stops idle instances automatically. When the next request arrives, it wakes them back up in under a second.

You write your app as if it serves one customer. tenement runs a copy for each of them.

alice.notes.example.com  ->  notes:alice  ->  isolated process + own database
bob.notes.example.com    ->  notes:bob    ->  isolated process + own database

Experimental. Actively developed. APIs may change.

Why this exists

systemd can run processes, but it doesn't route requests or stop idle ones. You'd write a unit file for each customer and wire up nginx yourself. Docker adds container overhead you don't need for trusted code on one machine. Kubernetes is absurd for a $5 VPS.

tenement is Fly Machines on your own hardware. Spawn a process, give it a subdomain, let it sleep when nobody's using it, wake it up on the next request.

systemd tenement
Routing You configure nginx per service alice.notes.example.com just works
Scale to zero Processes run forever Idle processes stop, wake on first request
Per-tenant data You manage it Each instance gets its own data directory
New customer Write a unit file, reload ten spawn notes:alice
Health + restart Basic restart-on-failure HTTP health checks, exponential backoff
Deployment Rolling restart scripts ten deploy notes:v2 then ten route --from v1 --to v2
Logs journalctl ten logs notes:alice with full-text search

Quick start

Install the CLI and start the server:

cargo install tenement-cli
ten serve --port 8080 --domain localhost
ten token-gen

Here's a complete app. It's a notes API backed by SQLite, and it doesn't know anything about tenants. It just reads PORT from the environment and serves whoever's asking.

# app.py
import os, json, sqlite3
from http.server import HTTPServer, BaseHTTPRequestHandler

PORT = int(os.environ["PORT"])
DB = os.path.join(os.environ.get("DATA_DIR", "."), "notes.db")

def get_db():
    os.makedirs(os.path.dirname(DB) or ".", exist_ok=True)
    db = sqlite3.connect(DB)
    db.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT)")
    return db

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/health":
            self.respond(200, {"status": "ok"})
        else:
            db = get_db()
            notes = [{"id": r[0], "text": r[1]} for r in db.execute("SELECT * FROM notes").fetchall()]
            db.close()
            self.respond(200, notes)

    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))))
        db = get_db()
        db.execute("INSERT INTO notes (text) VALUES (?)", (body["text"],))
        db.commit()
        db.close()
        self.respond(201, {"ok": True})

    def respond(self, code, data):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(data).encode())

HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()

The config is six lines. You tell tenement what command to run, where the health endpoint is, and what environment variables to set. The {id} in DATA_DIR gets replaced with the tenant name at spawn time.

# tenement.toml
[service.notes]
command = "python3 app.py"
health = "/health"
idle_timeout = 300
isolation = "process"

[service.notes.env]
DATA_DIR = "{data_dir}/{id}"

Now spawn a couple tenants and try it:

ten spawn notes:alice
ten spawn notes:bob

curl -X POST http://alice.notes.localhost:8080/notes \
  -H "Content-Type: application/json" -d '{"text":"hello from alice"}'

curl http://alice.notes.localhost:8080/notes   # alice's notes
curl http://bob.notes.localhost:8080/notes     # bob's notes (empty, different database)
ten ps                                         # list running instances

Alice and Bob each get their own process, their own SQLite database, their own data directory. After 5 minutes of no requests, tenement kills the process. The next request wakes it back up:

Runtime Cold wake (median)
Python ~65ms
Node.js ~105ms
Go (go run) ~140ms

How it works

You define a service in your config (the command, health endpoint, and environment variables). When you spawn an instance, tenement allocates a TCP port, sets PORT in the environment, and starts the process. Requests to alice.notes.example.com get proxied to alice's port. tenement polls the health endpoint and restarts unhealthy instances with exponential backoff. When nobody's made a request for a while, it kills the process. When someone does, it spawns a fresh one.

Your app handles its own auth, business logic, and data. tenement handles routing, lifecycle, and isolation. These two layers are completely independent, which means tenement doesn't touch your Authorization headers or care what framework you're using. You can verify this yourself with the auth-test example.

The economics

Most SaaS customers aren't active simultaneously. If you have 1000 customers and only 20 are using the product at any given moment, the traditional approach keeps all 1000 processes running. That's 20GB of RAM across 10 machines at maybe $500/month. With tenement, the 980 idle instances cost nothing. You run 20 processes on one machine for $5/month. The wake-on-request latency is under a second, so users don't notice.

This pairs well with SQLite. Each customer gets their own database file, replicated to S3 with something like walrust or Litestream. No shared Postgres, no connection pooling, no schema migrations that touch everyone's data at once.

What's in the box

Tenement does subdomain routing (alice.api.example.com routes to api:alice), scale-to-zero with wake-on-request, per-tenant data directories, process isolation via Linux namespaces, HTTP health checks with exponential backoff, weighted routing for blue-green and canary deployments, built-in TLS via Let's Encrypt, Prometheus metrics, log capture with full-text search, and a bearer token auth system for the management API with both admin and tenant-scoped tokens.

Commands like uv run python app.py or go run main.go are shell-split automatically, and every instance runs in its own process group so killing it also kills any child processes. No orphans.

CLI

ten serve --port 8080 --domain localhost    # start the server
ten spawn notes:alice                       # create a tenant
ten stop notes:alice                        # stop a tenant
ten ps                                      # list everything
ten logs notes:alice                        # tail logs
ten logs -f                                 # follow all logs
ten deploy notes:v2                         # deploy a new version
ten route notes --from v1 --to v2           # blue-green swap
ten weight notes:alice 50                   # canary: 50% traffic
ten token-gen                               # admin API token
ten token-gen --tenant alice                # scoped token for alice

Set TENEMENT_SERVER to skip passing --server on every command:

export TENEMENT_SERVER=http://localhost:9090
ten ps    # just works

Configuration

[settings]
data_dir = "./data"

[service.api]
command = "uv run python app.py"
health = "/health"
isolation = "process"            # "process" (macOS/Linux) or "namespace" (Linux, PID isolation)
idle_timeout = 300               # stop after 5 min idle
startup_timeout = 10             # increase for go run (30s)
storage_persist = true           # keep data across restarts
memory_limit_mb = 256            # cgroups limit (Linux)

[service.api.env]
DATA_DIR = "{data_dir}/{id}"     # {name}, {id}, {data_dir}, {port} all interpolate

Full reference at tenement.dev/guides/03-configuration.

Examples

The examples/ directory has complete working setups you can run immediately:

  • hello-world is the simplest possible setup, a bash script and netcat.
  • python-fastapi and node-fastify and go-http show the same pattern in three languages.
  • multi-runtime runs all three at once with a 56-test integration script that verifies auth, data isolation, and cross-service isolation.
  • auth-test demonstrates that tenement passes all request headers through untouched, so your app's auth works exactly as it would without tenement.
  • multi-tenant is a per-tenant notes API with SQLite, which is probably closest to what you'd actually build.

Production

For a Hetzner or DigitalOcean VPS with wildcard HTTPS:

# Point *.app.example.com at your server IP, then:
cargo install tenement-cli
cd /opt/myapp
ten init --name myapp --command "python3 app.py"
ten token-gen
ten install --domain app.example.com --caddy --dns-provider cloudflare

ten spawn myapp:customer1
ten spawn myapp:customer2
ten ps

The ten install command creates a systemd service and a Caddyfile with wildcard TLS. Caddy handles HTTPS, tenement handles everything else.

Development

cargo test    # 566 tests
cargo bench

Full docs at tenement.dev. See ROADMAP.md for what's next.

License

Apache 2.0

Metadata

Release files for tenement 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tenement 0.2.2
File Size Uploaded Provenance Actions
tenement-0.2.2.tar.gz 198.9 kB With provenance Details for tenement-0.2.2.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for tenement 0.2.2
File Actions
tenement-0.2.2-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details for tenement-0.2.2-py3-none-manylinux_2_28_aarch64.whl
tenement-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details for tenement-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
tenement-0.2.2-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details for tenement-0.2.2-py3-none-macosx_11_0_arm64.whl
tenement-0.2.2-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details for tenement-0.2.2-py3-none-macosx_10_12_x86_64.whl

Total release size: 31.1 MB

Release files / tenement-0.2.2.tar.gz

Download URL tenement-0.2.2.tar.gz
Size 198.9 kB
Tags Source
SHA-256 checksum
How to use checksums
e647049dfa74b8c9bbe4f14ee1bb262ca7801d8be13bf60bc3232ac3f2da108c
BLAKE2b-256 checksum
How to use checksums
b9b5e2cd61ec2d68ebdf08f240066ddfa3b0724fa637e9ac16098be5ad0d9125
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 7, 2026.

Transparency log

Release files / tenement-0.2.2-py3-none-manylinux_2_28_aarch64.whl

Download URL tenement-0.2.2-py3-none-manylinux_2_28_aarch64.whl
Size 7.5 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
17e3fb3b3b7108478da2214c5776067e4058f868f420da1f92f84ae28cc7b9c8
BLAKE2b-256 checksum
How to use checksums
a87d3cb2e2d1c1e53bcf9e53efb288d9d7aedbe895a7f1922324b43e46d0a32e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 7, 2026.

Transparency log

Release files / tenement-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL tenement-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 8.1 MB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
d7360fb68433c1f05ae928cb95dc2e14bae73e8c5e637507eb4870c9dec1b5a2
BLAKE2b-256 checksum
How to use checksums
1b8913c67945b8d41aaab09d385b56bc22f392c1494edd74d37bad060667c417
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 7, 2026.

Transparency log

Release files / tenement-0.2.2-py3-none-macosx_11_0_arm64.whl

Download URL tenement-0.2.2-py3-none-macosx_11_0_arm64.whl
Size 7.4 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
37c59ed016f32efbb4aee73a931e8bfb64e69b4474fa8fa2a0ca3ffce28d6f67
BLAKE2b-256 checksum
How to use checksums
41fd0f7b8301694698c73b3544ae071a8a746601ed8946a76d2d76e55fc863a2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 7, 2026.

Transparency log

Release files / tenement-0.2.2-py3-none-macosx_10_12_x86_64.whl

Download URL tenement-0.2.2-py3-none-macosx_10_12_x86_64.whl
Size 7.8 MB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
f457341af1ae0d72dbf610d9e79fa730c156979e1558f87756938ed0a3ca2eb5
BLAKE2b-256 checksum
How to use checksums
5a4df2c4916017e4f05d5b54464dcfb94125a3593b8b977a221e46e4ca9cc1fc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 7, 2026.

Transparency log

Security for tenement 0.2.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 5 files in this release have been uploaded with provenance metadata.

Select from 5 files

Attestation for tenement-0.2.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 7, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page