Skip to Content
Apso is in public beta. Get started

Scaling

Generated code gives you a standard framework service. Capacity planning still depends on request shape, database queries, tenant distribution, extension behavior, deployment resources, and reliability requirements.

Measure before changing the architecture

Collect evidence for:

  • Request rate by endpoint and tenant
  • Latency percentiles by endpoint
  • Error and timeout rates
  • Database query duration and rows scanned
  • Connection pool use and wait time
  • CPU and memory per application instance
  • Background job depth and age
  • Cache hit and eviction rates when a cache exists

Start with the slowest user workflow and the queries it produces. Adding instances will not repair an unindexed tenant query or an external provider call made in the request path.

Application instances

A generated service can run behind a load balancer when request state is externalized. Before adding replicas:

  1. Store shared sessions in the configured database or session store.
  2. Keep uploads in object storage rather than the instance filesystem.
  3. Keep scheduled work in a queue or scheduler with clear ownership.
  4. Configure graceful shutdown and readiness checks.
  5. Ensure each instance uses a bounded database pool.
Scale topologyAdd service capacity after shared state is ready.
Measured change
TrafficClient requests
RoutingLoad balancer
Instance 01Generated servicebounded pool • health checks
Instance 02Generated servicebounded pool • health checks
Durable statePostgreSQLrecords • sessions • migrations
Shared workCache or queuerepeated reads • background jobs
Keep uploads, sessions, scheduled work, and other shared state outside individual application instances.

Database capacity

The database is commonly the first shared limit.

Index tenant and lookup paths

Use .apsorc indexes for fields that appear in frequent filters, joins, or uniqueness checks. For a multi-tenant list route, the tenant field often belongs at the start of a composite index with the status or sort field used by the query.

.apsorc (entity excerpt)
{ "name": "Task", "scopeBy": "workspaceId", "fields": [ { "name": "status", "type": "enum", "values": ["Todo", "Done"], "index": true }, { "name": "dueDate", "type": "date", "nullable": true, "index": true } ] }

Review generated indexes and query plans against the actual filters. A single-field index is not always enough for a tenant-scoped sort.

Bound connection pools

Each application replica can open its own pool. Multiply the per-instance maximum by the maximum replica count and leave capacity for migrations, administration, and workers. Use a database pooler when many short-lived processes would otherwise consume too many connections.

Add replicas for measured read pressure

Read replicas require explicit application routing and consistency decisions. Put reporting or tolerant read workloads on a replica after verifying that replication lag is acceptable. Keep writes and read-after-write workflows on the primary.

Caching

Add a cache around repeated, expensive reads with a clear invalidation rule. Good candidates include stable reference data or expensive aggregates. Tenant-owned records require tenant-aware cache keys.

workspace:{workspaceId}:project:{projectId}

Do not cache authorization decisions longer than the underlying membership or role can safely remain stale.

Background work

Move email, webhooks, exports, media processing, and other retryable work out of the HTTP response path when the product does not require synchronous completion. Implement producers and workers in extensions/ and define:

  • Idempotency key
  • Retry policy
  • Dead-letter handling
  • Tenant context
  • Observability fields
  • Maximum execution time

Load testing

Use a representative schema and data distribution. Include joins, tenant scope, authentication, and extension calls in the test. Increase load gradually and record the first constrained resource. Test deployment and database limits in a non-production environment with the same relevant configuration.

Do not adopt generic latency, throughput, or replica targets from documentation. Set service objectives from the product workflow and validate them in the environment that will run the service.

Scaling checklist

  1. Define a user-facing service objective.
  2. Add tracing or structured timing around the workflow.
  3. Inspect database query plans and indexes.
  4. Bound application and database concurrency.
  5. Add replicas only when the process is ready for shared-state execution.
  6. Add cache or workers for a measured workload.
  7. Load test the change and compare the same metrics.
  8. Document rollback and failure behavior.
Last updated on