Scaling Model
This page describes how Causet scales today and where the current limits are.
Current State
Causet’s current architecture uses a shared infrastructure model:
- One
causet.projection-events.v1Kafka topic shared by all tenants. - One
projection-workerconsumer group processing all projection events. - One
projectionsPostgreSQL database with per-tenant schema isolation. - One
query-servicepool reading from the projections database. - One
causetPostgreSQL database storing all ledger events.
Tenant isolation is logical (via schema isolation and fork IDs), not physical.
Write Path Scaling
causet-runtime horizontal scaling
Multiple causet-runtime instances can run simultaneously. Write coordination is handled by a per-entity cursor lock in the causet database — not in-process. This means:
- Two runtime instances can safely process intents for different entities concurrently.
- Two runtime instances will contend on the cursor lock for the same entity (one will wait).
- There is no single runtime process as a bottleneck for cross-entity writes.
Scale causet-runtime by adding instances. Route load via a load balancer. Kafka consumer instances for causet.intents.v1 can be added to the same consumer group.
Per-entity throughput limit
Each entity is serialized through a cursor lock. The throughput ceiling for a single entity is determined by:
- Round-trip time to acquire the lock in PostgreSQL
- Rule evaluation time
- Ledger write latency
For most domains this is not a bottleneck. High-frequency write entities (e.g. a global counter updated on every request) are a known hot-aggregate problem.
Projection Worker Scaling
The projection-worker consumer group scales horizontally up to the number of partitions in causet.projection-events.v1. Each worker instance is assigned a subset of partitions.
- Adding worker instances reduces per-instance partition load.
- Worker instances beyond the partition count are idle (Kafka does not assign more instances than partitions).
- Projection processing for a given entity remains ordered — all events for entity X go to the same partition.
Configure causet.projection-events.v1 with enough partitions (12–24+) to support the target number of worker instances.
Read Path Scaling
Query service
query-service is stateless — it reads from the projections database. Scale by adding instances behind a load balancer.
Redis result cache
Named queries with a cache_ttl declared in the DSL are cached in Redis. High-read queries benefit significantly from result caching. The Redis cluster can be scaled independently.
Read replicas
The projections PostgreSQL database supports read replicas. query-service can be configured to route reads to a replica pool, keeping the primary for projection-worker writes only.
Known Bottlenecks
| Bottleneck | Description | Mitigation |
|---|---|---|
| Hot aggregates | Single high-write entity contends on cursor lock | Shard entity IDs; reduce write frequency via batching |
| Shared projections DB | All tenants write to same DB instance | Per-tenant DB (roadmap); connection pooling (PgBouncer) |
| Kafka single topic | All tenants share one projection topic | Per-tenant topics (roadmap) |
| Schema count | PostgreSQL has limits on schema count per database | Per-tenant DB sharding (roadmap) |
Partition Keys
causet.projection-events.v1 is partitioned by entity_id. This ensures:
- All events for the same entity go to the same partition.
- Per-entity ordering is maintained at the consumer.
- Projection writes for the same entity are sequential within a worker instance.
Choose entity IDs that are well-distributed (e.g. UUIDs or high-cardinality strings) to avoid partition hotspots.
Cursor Lock and Multi-Instance Safety
The cursor lock mechanism is implemented as an advisory lock (or row-level lock) in the causet PostgreSQL database. It is:
- Database-level, not in-process.
- Automatically released on connection close or transaction rollback.
- Safe for multiple runtime instances to contend on.
This means causet-runtime is safe to scale horizontally without a distributed coordination layer (no ZooKeeper, no Redis locks needed for the write path).
Read Replicas and Caching
causet-runtime → causet DB (primary) [writes]
[replica] → not currently used
projection-worker → projections DB (primary) [writes]
query-service → projections DB (primary or replica) [reads]
→ Redis (result cache) [reads, optional]Roadmap
Note: The following items are planned but not yet implemented.
| Item | Description |
|---|---|
| Per-tenant Kafka topics | Isolate high-volume tenants to their own topics for independent scaling and retention |
| Shard routing | Route tenants to different projections DB instances based on a shard key |
| Connection pooling | PgBouncer or similar in front of projections DB to reduce connection overhead at scale |
| Per-fork projection worker | Dedicated worker instances for isolated forks (e.g. production vs. all others) |
| Projection scaling plan | See the PROJECTION_SCALING_PLAN internal document for the full design |
Related Pages
- Multi-Tenancy — per-fork schema isolation
- Message Brokers — Kafka partition configuration
- Projection Pipeline — consumer group details
- Storage — database access patterns