Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs/deployment/rate-limits.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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. |

<CodeGroup group="docker">
<Tab title="With Docker" value="with-docker">
```bash
docker run \
-p 3567:3567 \
-e MAX_SERVER_POOL_SIZE=20 \
-e MAX_CONCURRENT_REQUESTS_PER_CUD=8 \
-d "$SUPERTOKENS_IMAGE"
```
</Tab>
<Tab title="Without Docker" value="without-docker">
```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
```
</Tab>
</CodeGroup>

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.
45 changes: 45 additions & 0 deletions docs/deployment/scalability.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<CodeGroup group="docker">
<Tab title="With Docker" value="with-docker">
```bash
docker run \
-p 3567:3567 \
-e STORAGE_INIT_TIMEOUT_SECONDS=60 \
-e STORAGE_INIT_PARALLELISM=8 \
-d "$SUPERTOKENS_IMAGE"
```
</Tab>
<Tab title="Without Docker" value="without-docker">
```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
```
</Tab>
</CodeGroup>

---

## Database
Expand Down
2 changes: 2 additions & 0 deletions docs/deployment/self-host-supertokens.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
:::


Expand Down
143 changes: 143 additions & 0 deletions docs/platform-configuration/supertokens-core/admin-port.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
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.

<CodeGroup group="docker">
<Tab title="With Docker" value="with-docker">
```bash
docker run \
-p 3567:3567 \
-p 3568:3568 \
-e SUPERTOKENS_ADMIN_PORT=3568 \
-d "$SUPERTOKENS_IMAGE"
```
</Tab>
<Tab title="Without Docker" value="without-docker">
```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
```
</Tab>
</CodeGroup>

| 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.

`/global-request-stats` requires one of the base configuration's `api_keys`; an app's own API key doesn't unlock it.

---

## 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. |

<CodeGroup group="docker">
<Tab title="With Docker" value="with-docker">
```bash
docker run \
-p 3567:3567 \
-p 3568:3568 \
-e SUPERTOKENS_ADMIN_PORT=3568 \
-e ADMIN_ONLY_PATHS="/ee/license" \
-d "$SUPERTOKENS_IMAGE"
```
</Tab>
<Tab title="Without Docker" value="without-docker">
```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"
```
</Tab>
</CodeGroup>

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.
7 changes: 6 additions & 1 deletion scripts/blume/route-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"counts": {
"pages": 429,
"pages": 430,
"openapiOperations": 205
},
"pages": [
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion scripts/migration/legacy-redirect-matrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading