From 513b3e6d0e420c1e2d9136464d8e5a30e858ffff Mon Sep 17 00:00:00 2001 From: tamassoltesz Date: Tue, 20 Oct 2026 15:50:41 +0200 Subject: [PATCH 1/2] docs: document the core 12.4 configs (admin port, concurrency cap, storage init) Co-Authored-By: Claude Opus 5.5 --- docs/deployment/rate-limits.mdx | 57 +++++++ docs/deployment/scalability.mdx | 45 ++++++ docs/deployment/self-host-supertokens.mdx | 2 + .../supertokens-core/admin-port.mdx | 141 ++++++++++++++++++ scripts/blume/route-manifest.json | 7 +- .../migration/legacy-redirect-matrix.test.ts | 2 +- 6 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 docs/platform-configuration/supertokens-core/admin-port.mdx diff --git a/docs/deployment/rate-limits.mdx b/docs/deployment/rate-limits.mdx index dd00257e5..6860daff8 100644 --- a/docs/deployment/rate-limits.mdx +++ b/docs/deployment/rate-limits.mdx @@ -38,6 +38,8 @@ This is independent to the rate limit described above, and cannot change. ## For self hosted The SuperTokens core has no rate limit other than for the `/hello` API (which is 5 requests per second per app). +You can optionally cap concurrent requests per connection URI domain, as described in +[Concurrency cap per connection URI domain](#concurrency-cap-per-connection-uri-domain). You are free to add rate limits to the core by using [a reverse proxy like Nginx](https://www.nginx.com/blog/rate-limiting-nginx/). If you want to implement rate limiting policy similar to the managed service described above, add the following to your `http` and `server` block in the `nginx.conf` file: @@ -75,3 +77,58 @@ http { ``` In the above, the core adds a rate limit per app per IP address. + +### Concurrency cap per connection URI domain + +The concurrency cap requires Core `12.4.0` or later and is off by default. + +A connection URI domain is the host name that backends use to reach the core. +A self-hosted core usually has a single one, which all its apps share. + +The cap limits how many requests from one connection URI domain the core processes at the same time, so that one domain +can't take all the threads of the request pool (`max_server_pool_size`). +The core enforces it only while the pool is close to full: + +- While more than `concurrency_cap_reserved_pool_percent` of the pool's threads are free, the core rejects no request. +- Once fewer threads are free, a request that would take its connection URI domain above + `max_concurrent_requests_per_cud` requests in flight gets an immediate `429` response with a `Retry-After: 1` header, + instead of a thread. + +For example, with `max_server_pool_size: 10` and the default reserve of 25%, the core keeps 2 threads in reserve and +enforces the cap once more than 8 requests are in flight. + +Only requests that pass the API key check for an app that exists under the connection URI domain count toward its cap. +Requests to routes without an API key check, such as `/hello` and `/.well-known/jwks.json`, and requests for an app that +doesn't exist count toward the pool's usage but are never capped. +Requests on the [admin port](/platform-configuration/supertokens-core/admin-port) don't count at all. + +| Configuration | Environment variable | Default | Description | +| --- | --- | --- | --- | +| `max_concurrent_requests_per_cud` | `MAX_CONCURRENT_REQUESTS_PER_CUD` | `0` | Maximum concurrent requests per connection URI domain. `0` turns the cap off. Can't exceed `max_server_pool_size`. | +| `concurrency_cap_reserved_pool_percent` | `CONCURRENCY_CAP_RESERVED_POOL_PERCENT` | `25` | Share of `max_server_pool_size`, in percent, kept free before the cap applies. Must be between 1 and 100. `100` enforces the cap at all times. | + + + +```bash +docker run \ + -p 3567:3567 \ + -e MAX_SERVER_POOL_SIZE=20 \ + -e MAX_CONCURRENT_REQUESTS_PER_CUD=8 \ + -d "$SUPERTOKENS_IMAGE" +``` + + +```yaml +# You need to add the following to the config.yaml file. +# The file path can be found by running the "supertokens --help" command + +max_server_pool_size: 20 +max_concurrent_requests_per_cud: 8 +``` + + + +The backend SDK retries requests that get a `429` response. +The core's `/requests/stats` API reports `concurrentRequestsRejected`, the number of an app's requests that got a `429` +from the cap, and `concurrentRequestsInFlight`, which covers the whole connection URI domain, so every app on the same +domain reports the same value. diff --git a/docs/deployment/scalability.mdx b/docs/deployment/scalability.mdx index 26fcfb3bf..d3372c977 100644 --- a/docs/deployment/scalability.mdx +++ b/docs/deployment/scalability.mdx @@ -62,6 +62,51 @@ If you are facing performance issues, here are some tips to help you tune the pe - Check if you have access token blacklisting enabled in the backend SDK. The default is `false`, but if you have it enabled, then it means that every session verification attempt queries the SuperTokens core to check the database. This adds latency to the session verification process and increases the load on the core. If you want to keep this to `true`, consider making it only for non `GET` APIs for your application. - You can increase the value of `access_token_validity` in the SuperTokens core. It sets the validity of the access token. Default value is 3,600 seconds (1 hour). The lower this value, the more often the refresh API calls the core, increasing the load on the core. +### Startup with many tenant databases + +The settings in this section require Core `12.4.0` or later. +They matter when apps or tenants use their own databases, because the core sets up a connection pool for each database +when it starts. + +The core initialises the base database first, without a time limit. +It then initialises the other databases in parallel, `storage_init_parallelism` at a time: + +- A database that is still initialising after `storage_init_timeout_seconds`, counted from when its own initialisation + started, is logged as an error. Startup continues without waiting for it, and the database keeps initialising in the + background. Requests for the apps and tenants that use it wait until it is ready. +- Each such slow database lets one more database start, so more than `storage_init_parallelism` databases can be + initialising while slow ones are still running. +- A database the core can't connect to is logged, and the core starts without it. + +| Configuration | Environment variable | Default | Description | +| --- | --- | --- | --- | +| `storage_init_timeout_seconds` | `STORAGE_INIT_TIMEOUT_SECONDS` | `120` | Seconds startup waits for each tenant database. Must be at least 1. | +| `storage_init_parallelism` | `STORAGE_INIT_PARALLELISM` | `16` | Number of tenant databases initialised at the same time. Must be at least 1. | + +Lower `storage_init_parallelism` if many tenants share one database server, so that startup opens fewer connections to +it at once. Raise it if the tenants use many separate database servers and startup takes too long. + + + +```bash +docker run \ + -p 3567:3567 \ + -e STORAGE_INIT_TIMEOUT_SECONDS=60 \ + -e STORAGE_INIT_PARALLELISM=8 \ + -d "$SUPERTOKENS_IMAGE" +``` + + +```yaml +# You need to add the following to the config.yaml file. +# The file path can be found by running the "supertokens --help" command + +storage_init_timeout_seconds: 60 +storage_init_parallelism: 8 +``` + + + --- ## Database diff --git a/docs/deployment/self-host-supertokens.mdx b/docs/deployment/self-host-supertokens.mdx index c45c7b998..7a37088de 100644 --- a/docs/deployment/self-host-supertokens.mdx +++ b/docs/deployment/self-host-supertokens.mdx @@ -158,6 +158,8 @@ If you are having issues with starting the docker image, please feel free to rea exhausted, it can return `200 Hello` without querying storage. It also deliberately requires no API key. Use it only as a basic process/readiness signal, not as proof of database health, API-key enforcement, or safe network exposure. Pair it with authenticated application checks and database monitoring; tune liveness separately to avoid restart loops. +For a liveness check that doesn't read the database, use `/livez` on the +[admin port](/platform-configuration/supertokens-core/admin-port) (Core `12.4.0` or later). ::: diff --git a/docs/platform-configuration/supertokens-core/admin-port.mdx b/docs/platform-configuration/supertokens-core/admin-port.mdx new file mode 100644 index 000000000..5bb91a0c8 --- /dev/null +++ b/docs/platform-configuration/supertokens-core/admin-port.mdx @@ -0,0 +1,141 @@ +--- +title: Serve health checks on an admin port +description: Run liveness checks and control-plane routes on a separate SuperTokens Core port with its own thread pool. +sidebar: + order: 3 +--- + +## Overview + +SuperTokens Core can open a second port, the admin port, with its own request thread pool. +Liveness checks and control-plane routes on that port keep responding when the main port's thread pool is busy with +application traffic. + +The admin port requires Core `12.4.0` or later. + +## Before you start + +:::warning +This page is only relevant if you are self-hosting SuperTokens. +If you use the managed service, SuperTokens runs the core and its health checks for you. +::: + +:::caution[The admin port is not a separate security boundary] +The admin port applies the same `api_keys` and IP filter rules as the main port, binds to the same `host`, and has no +credential of its own. +It serves sensitive routes, such as `/ee/license`, which sets the license key, and `/global-request-stats`. +Expose it only to the network where your orchestrator and control plane run. +::: + +--- + +## Enable the admin port + +Set `admin_port` to a port that differs from the main `port` (default `3567`). +If you don't set it, Core runs a single port and serves every route on it. + + + +```bash +docker run \ + -p 3567:3567 \ + -p 3568:3568 \ + -e SUPERTOKENS_ADMIN_PORT=3568 \ + -d "$SUPERTOKENS_IMAGE" +``` + + +```yaml +# You need to add the following to the config.yaml file. +# The file path can be found by running the "supertokens --help" command + +admin_port: 3568 +``` + + + +| Configuration | Environment variable | Default | Description | +| --- | --- | --- | --- | +| `admin_port` | `SUPERTOKENS_ADMIN_PORT` | not set | Port of the admin connector. Must be between 1 and 65535 and differ from `port`. | +| `admin_max_server_pool_size` | `ADMIN_MAX_SERVER_POOL_SIZE` | `5` | Maximum number of threads for requests on the admin port. Must be at least 1. | + +Requests on the admin port don't count toward the request pool of the main port, and the +[concurrency cap](/deployment/rate-limits#concurrency-cap-per-connection-uri-domain) doesn't apply to them. + +--- + +## Which routes each port serves + +With the admin port enabled, each route belongs to one of three groups: + +| Route | Main port | Admin port | +| --- | --- | --- | +| `/livez`, `/global-request-stats` | No | Yes | +| `/hello`, `/ee/license`, `/ee/featureflag` | Yes | Yes | +| All other routes | Yes | No | + +A request for a route on a port that doesn't serve it gets a `404` response. + +--- + +## Health checks + +Use the two health check routes for different purposes: + +- `GET /livez` is a liveness check. It returns `200 OK` without reading the database, without an API key, and without a + `cdi-version` header. Point your container's liveness probe at `/livez` on the admin port, so that a slow database or a + busy main port doesn't get a healthy process restarted. +- `GET /hello` reads from the database and works as a readiness check. See + [Self-host SuperTokens](/deployment/self-host-supertokens#2-test-that-the-service-is-running) for its limits. + +For example, in Kubernetes: + +```yaml +livenessProbe: + httpGet: + path: /livez + port: 3568 +readinessProbe: + httpGet: + path: /hello + port: 3567 +``` + +--- + +## Change which port serves a route + +Use these configurations to move a route to the admin port or to serve it on both ports: + +| Configuration | Environment variable | Description | +| --- | --- | --- | +| `admin_only_paths` | `ADMIN_ONLY_PATHS` | Comma-separated API paths to serve only on the admin port. | +| `admin_preferred_paths` | `ADMIN_PREFERRED_PATHS` | Comma-separated API paths to serve on both ports. | + + + +```bash +docker run \ + -p 3567:3567 \ + -p 3568:3568 \ + -e SUPERTOKENS_ADMIN_PORT=3568 \ + -e ADMIN_ONLY_PATHS="/ee/license" \ + -d "$SUPERTOKENS_IMAGE" +``` + + +```yaml +# You need to add the following to the config.yaml file. +# The file path can be found by running the "supertokens --help" command + +admin_port: 3568 +admin_only_paths: "/ee/license" +``` + + + +Core refuses to start if: + +- One of the paths doesn't match a Core API path. +- The same path appears in both lists. +- `admin_port` is not set. diff --git a/scripts/blume/route-manifest.json b/scripts/blume/route-manifest.json index cc35a5ee5..e8db6e155 100644 --- a/scripts/blume/route-manifest.json +++ b/scripts/blume/route-manifest.json @@ -1,6 +1,6 @@ { "counts": { - "pages": 429, + "pages": 430, "openapiOperations": 205 }, "pages": [ @@ -739,6 +739,11 @@ "route": "/platform-configuration/supertokens-core/add-ssl-via-nginx", "publicRoute": "/docs/platform-configuration/supertokens-core/add-ssl-via-nginx" }, + { + "file": "docs/platform-configuration/supertokens-core/admin-port.mdx", + "route": "/platform-configuration/supertokens-core/admin-port", + "publicRoute": "/docs/platform-configuration/supertokens-core/admin-port" + }, { "file": "docs/platform-configuration/supertokens-core/api-keys.mdx", "route": "/platform-configuration/supertokens-core/api-keys", diff --git a/scripts/migration/legacy-redirect-matrix.test.ts b/scripts/migration/legacy-redirect-matrix.test.ts index 78892b8b2..246384ad2 100644 --- a/scripts/migration/legacy-redirect-matrix.test.ts +++ b/scripts/migration/legacy-redirect-matrix.test.ts @@ -12,7 +12,7 @@ const redirects = new Map(legacyRedirects.map(({ from, to }) => [from, to])); describe("generated legacy redirect matrix", () => { it("locks generated artifact counts", () => { - expect(routeManifest.counts).toEqual({ pages: 429, openapiOperations: 205 }); + expect(routeManifest.counts).toEqual({ pages: 430, openapiOperations: 205 }); expect(legacyRedirects).toHaveLength(1566); expect(legacyRedirectDiagnostics.retained).toHaveLength(1566); expect(legacyRedirectDiagnostics.ignored).toHaveLength(7); From cd1747d3e86707b7ab539c9d0a2fedbb1f018998 Mon Sep 17 00:00:00 2001 From: tamassoltesz Date: Tue, 20 Oct 2026 16:36:58 +0200 Subject: [PATCH 2/2] docs: note the credential /global-request-stats requires Co-Authored-By: Claude Opus 5.5 --- docs/platform-configuration/supertokens-core/admin-port.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/platform-configuration/supertokens-core/admin-port.mdx b/docs/platform-configuration/supertokens-core/admin-port.mdx index 5bb91a0c8..861b0a5ae 100644 --- a/docs/platform-configuration/supertokens-core/admin-port.mdx +++ b/docs/platform-configuration/supertokens-core/admin-port.mdx @@ -76,6 +76,8 @@ With the admin port enabled, each route belongs to one of three groups: A request for a route on a port that doesn't serve it gets a `404` response. +`/global-request-stats` requires one of the base configuration's `api_keys`; an app's own API key doesn't unlock it. + --- ## Health checks