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.v1 Kafka topic shared by all tenants.
  • One projection-worker consumer group processing all projection events.
  • One projections PostgreSQL database with per-tenant schema isolation.
  • One query-service pool reading from the projections database.
  • One causet PostgreSQL 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

BottleneckDescriptionMitigation
Hot aggregatesSingle high-write entity contends on cursor lockShard entity IDs; reduce write frequency via batching
Shared projections DBAll tenants write to same DB instancePer-tenant DB (roadmap); connection pooling (PgBouncer)
Kafka single topicAll tenants share one projection topicPer-tenant topics (roadmap)
Schema countPostgreSQL has limits on schema count per databasePer-tenant DB sharding (roadmap)

Partition Keys

causet.projection-events.v1 is partitioned by entity_id. This ensures:

  1. All events for the same entity go to the same partition.
  2. Per-entity ordering is maintained at the consumer.
  3. 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.

ItemDescription
Per-tenant Kafka topicsIsolate high-volume tenants to their own topics for independent scaling and retention
Shard routingRoute tenants to different projections DB instances based on a shard key
Connection poolingPgBouncer or similar in front of projections DB to reduce connection overhead at scale
Per-fork projection workerDedicated worker instances for isolated forks (e.g. production vs. all others)
Projection scaling planSee the PROJECTION_SCALING_PLAN internal document for the full design