Scaling

Authagonal is designed to scale both vertically and horizontally with no special configuration.

Stateless by design

All persistent state is stored in Azure Table Storage. There is no in-process state that requires sticky sessions or coordination between instances:

ASP.NET Core’s Data Protection keys are automatically persisted to Azure Blob Storage when using a real Azure Storage connection string. This means cookies signed by one instance can be decrypted by any other instance — no sticky sessions required.

For local development with Azurite, data protection keys fall back to the default file-based store.

You can also point to an explicit blob URI via configuration:

{
  "DataProtection": {
    "BlobUri": "https://youraccount.blob.core.windows.net/dataprotection/keys.xml"
  }
}

Per-instance caches

A small number of read-heavy, slow-changing values are cached in memory per instance to reduce Table Storage round-trips:

Data Cache duration Impact of staleness
OIDC discovery documents 60 minutes (configurable) Delayed awareness of IdP key rotation
SAML IdP metadata 60 minutes (configurable) Same
CORS allowed origins 60 minutes (configurable) New origins take up to an hour to propagate

These caches are acceptable for production use. All durations are configurable via the Cache configuration section — see Configuration. If you need immediate propagation, restart the affected instances.

Rate limiting

Registration endpoints are protected by a built-in distributed rate limiter (5 registrations per IP per hour). When running multiple instances, rate limit counts are automatically shared between all instances via a gossip protocol — no external coordination required.

How it works

Each instance maintains its own counters in memory using a CRDT G-Counter. Instances discover each other via UDP multicast and exchange state over HTTP every few seconds. The consolidated count across all instances is used to make rate limiting decisions.

This means rate limits are enforced globally: if a client hits 3 different instances, all 3 know the total is 3, not 1 each.

Node identity

Each instance generates a random hex node ID at startup (e.g., a3f1b2). This ID identifies the instance in gossip messages and rate limit state. It is not persisted — a new ID is generated on each restart.

A ClusterLeaderService runs on each instance, electing a single leader among discovered peers (lowest node ID wins). Leadership transfers automatically when the leader dies. The leader is used for cluster-wide coordination — currently, signing key rotation (when enabled) runs only on the leader to avoid concurrent key generation.

Cluster configuration

Clustering is enabled by default with zero configuration. Instances on the same network discover each other automatically via UDP multicast (239.42.42.42:19847).

For environments where multicast is unavailable (some cloud VPCs), configure a load-balanced internal URL as a fallback:

{
  "Cluster": {
    "InternalUrl": "http://authagonal-auth.svc.cluster.local:8080",
    "Secret": "shared-secret-here"
  }
}

To disable clustering entirely (local-only rate limiting):

{
  "Cluster": {
    "Enabled": false
  }
}

See the Configuration page for all cluster settings.

Graceful degradation

Multi-tenant deployments

In multi-tenant mode (AddAuthagonalCore()), background services like GrantReconciliationService and SigningKeyRotationService are not registered — the host manages these per-tenant. Only TokenCleanupService runs unconditionally.

Scaling recommendations

Vertical scaling — increase CPU and memory on a single instance. Useful for handling more concurrent requests per instance.

Horizontal scaling — run multiple instances behind a load balancer. No sticky sessions or shared caches required. Each instance is fully independent.

Scale to zero — Authagonal supports scale-to-zero deployments (e.g., Azure Container Apps with minReplicas: 0). The first request after idle will have a cold start of a few seconds while the .NET runtime initializes and signing keys are loaded from storage.