Compare commits

...

2 Commits

Author SHA1 Message Date
a351565ff0 website: Refine calculator. 2025-05-28 15:22:56 +02:00
d583ddf0a3 website/docs: add scaling page
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

fix typing, re-style a bit, fix some react errors

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

disable autocomplete

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

minor grammar fixes

Signed-off-by: Fletcher Heisler <fheisler@users.noreply.github.com>

try to fix stuff

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-05-27 19:25:32 +02:00
4 changed files with 552 additions and 0 deletions

View File

@ -0,0 +1,50 @@
---
title: Scaling
---
import ScalingCalculator from "@site/src/components/ScalingCalculator";
authentik can be scaled to several thousands of users. This page aims to provide some guidance on installation sizing and tuning for those large installations.
:::note
The numbers indicated on this page are meant as general guidelines and starting points. Usage patterns vary from one installation to another, and we are constantly optimizing authentik to improve its performance.
The data used to build this page has been collected from benchmarks run on version 2024.6, and from real-world customer feedback.
:::
## How does authentik scale?
### authentik server
The authentik server is a stateless component that serves HTTP requests. As such, it is completely stateless and scales linearly. That means that if a single authentik server with X resources (CPU and RAM) can serve N requests, two authentik server with each X resources can serve 2N requests.
### authentik worker
The authentik worker is a stateless component that processes background tasks for authentik. This includes:
- maintenance tasks, such as removing outdated resources
- synchronization tasks for sources and providers such as SCIM, Google Workspace, LDAP, etc.
- one-off tasks that are triggered by some action by a user or an administrator, such as sending notifications.
Scaling the worker thus greatly depends on the usage made of authentik. If not synchronization is configured and there are few one-off tasks, then the worker will use almost no resources. However, if the worker has to process lengthy synchronization tasks and is then backlogged with other tasks, then it needs to be scaled up.
As such, we recommend setting up some [monitoring](../sys-mgmt/monitoring.md) and observing how the worker behaves, then scaling it up accordingly. You should aim to have as few tasks as possible in the worker queue.
### PostgreSQL & Redis
We recommend looking into both tools' respective documentation to get pointers on how to monitor them.
The specific usage that authentik makes of PostgreSQL and Redis doesn't scale linearly, but rather logarithmically.
## Calculator
<ScalingCalculator />
#### Known issues
The database recommendations given by the calculator are scaled linearly to the number of concurrent logins. This is not the pattern we observe in reality. As such, if the calculator returns ludicrous values for the database sizing, expect those to not be representative of the actual resources needed.
## Feedback
We welcome feedback on this calculator. [Contact us!](mailto:hello@goauthentik.io)

View File

@ -123,6 +123,7 @@ const items = [
"install-config/reverse-proxy",
"install-config/automated-install",
"install-config/air-gapped",
"install-config/scaling",
],
},
{

View File

@ -0,0 +1,374 @@
import Link from "@docusaurus/Link";
import Translate from "@docusaurus/Translate";
import Admonition from "@theme/Admonition";
import React, { useCallback, useMemo, useState } from "react";
import styles from "./style.module.css";
type RowID =
| "replicas"
| "requests_cpu"
| "requests_memory"
| "gunicorn_workers"
| "gunicorn_threads";
const rows: [rowID: RowID, rowLabel: JSX.Element, units?: string][] = [
[
// ---
"replicas",
<Translate id="ak.scalingCalculator.replicas">Replicas</Translate>,
],
[
// ---
"requests_cpu",
<Translate id="ak.scalingCalculator.requestsCpu">CPU Requests</Translate>,
],
[
// ---
"requests_memory",
<Translate id="ak.scalingCalculator.requestsMemory">Memory Requests</Translate>,
"GB",
],
[
// ---
"gunicorn_workers",
<Link to="./configuration#authentik_web__workers">
<Translate>Gunicorn Workers</Translate>
</Link>,
],
[
// ---
"gunicorn_threads",
<Link to="./configuration#authentik_web__threads">
<Translate>Gunicorn Threads</Translate>
</Link>,
],
];
type SetupEstimate = {
[key in RowID]: number;
};
type SetupEntry = [columnLabel: React.ReactNode, estimate: SetupEstimate];
const FieldName = {
UserCount: "userCount",
ConcurrentLogins: "loginCount",
FlowDuration: "flowDuration",
} as const satisfies Record<string, string>;
type FieldKey = keyof typeof FieldName;
type FieldName = (typeof FieldName)[FieldKey];
type EstimateInput = { [key in FieldName]: number };
type FieldID = `${FieldName}-field`;
const FieldID = Object.fromEntries(
Object.entries(FieldName).map(([key, value]) => [key, `${value}-field`]),
) as Record<FieldKey, FieldID>;
const SetupComparisionTable: React.FC<EstimateInput> = ({ loginCount }) => {
const cpuCount = Math.max(1, Math.ceil(loginCount / 10));
const setups: SetupEntry[] = [
[
<Translate
id="ak.setup.kubernetesRAMOptimized"
values={{ platform: "Kubernetes", variant: "(RAM Optimized)", separator: <br /> }}
>
{"{platform}{separator}{variant}"}
</Translate>,
{
gunicorn_threads: 2,
gunicorn_workers: 3,
replicas: Math.max(2, Math.ceil(cpuCount / 2)),
requests_cpu: 2,
requests_memory: 1.5,
},
],
[
<Translate
id="ak.setup.kubernetesCPUOptimized"
values={{ platform: "Kubernetes", variant: "(CPU Optimized)", separator: <br /> }}
>
{"{platform}{separator}{variant}"}
</Translate>,
{
gunicorn_threads: 2,
gunicorn_workers: 2,
replicas: Math.max(2, cpuCount),
requests_cpu: 1,
requests_memory: 1,
},
],
[
<Translate
id="ak.setup.dockerVM"
values={{
platform: "Docker Compose",
variant: "(Virtual machine)",
separator: <br />,
}}
>
{"{platform}{separator}{variant}"}
</Translate>,
{
gunicorn_threads: 2,
gunicorn_workers: cpuCount + 1,
replicas: Math.max(2, cpuCount),
requests_cpu: cpuCount,
requests_memory: cpuCount,
},
],
];
return (
<Admonition type="tip" icon={null} title={null} className={styles.admonitionTable}>
<div
className={styles.comparisionTable}
style={
{ "--ak-comparision-table-columns": setups.length + 1 } as React.CSSProperties
}
>
<header>
<div className={styles.columnLabel}>
<Translate id="ak.scalingCalculator.server">Resources</Translate>
</div>
{setups.map(([columnLabel], i) => (
<div className={styles.columnLabel} key={i}>
{columnLabel}
</div>
))}
</header>
{rows.map(([rowID, rowLabel, units]) => {
return (
<section key={rowID}>
<div className={styles.rowLabel}>{rowLabel}</div>
{setups.map(([_rowLabel, estimate], i) => {
const estimateValue = estimate[rowID] || "N/A";
return (
<div className={styles.fieldValue} key={i}>
<Translate
id={`ak.scalingCalculator.${rowID}`}
values={{
value: estimateValue,
units: units ? ` ${units}` : "",
}}
>
{"{value}{units}"}
</Translate>
</div>
);
})}
</section>
);
})}
</div>
</Admonition>
);
};
export const DatabaseEstimateTable: React.FC<EstimateInput> = ({ loginCount, userCount }) => {
const cpuCount = Math.max(1, Math.ceil(loginCount / 10));
const postgres = {
cpus: Math.max(2, cpuCount / 4),
ram: Math.max(4, cpuCount),
storage_gb: Math.ceil(userCount / 25000),
};
const redis = {
cpus: Math.max(2, cpuCount / 4),
ram: Math.max(2, cpuCount / 2),
};
return (
<Admonition type="tip" icon={null} title={null} className={styles.admonitionTable}>
<div
className={styles.comparisionTable}
style={{ "--ak-comparision-table-columns": 3 } as React.CSSProperties}
>
<header>
<div className={styles.columnLabel}>
<Translate id="ak.scalingCalculator.server">Resources</Translate>
</div>
<div className={styles.columnLabel}>PostgreSQL</div>
<div className={styles.columnLabel}>Redis</div>
</header>
<section>
<div className={styles.rowLabel}>CPUs</div>
<div className={styles.fieldValue}>{postgres.cpus}</div>
<div className={styles.fieldValue}>{redis.cpus}</div>
</section>
<section>
<div className={styles.rowLabel}>Memory</div>
<div className={styles.fieldValue}>{postgres.ram} GB</div>
<div className={styles.fieldValue}>{redis.ram} GB</div>
</section>
<section>
<div className={styles.rowLabel}>Storage</div>
<div className={styles.fieldValue}>{postgres.storage_gb} GB</div>
<div className={styles.fieldValue}>
<Translate id="ak.scalingCalculator.varies">Varies</Translate>
</div>
<div />
</section>
</div>
</Admonition>
);
};
export const ScalingCalculator: React.FC = () => {
const [estimateInput, setEstimateInput] = useState<EstimateInput>(() => {
const userCount = 100;
const flowDuration = 15;
return {
userCount,
flowDuration,
loginCount: -1,
};
});
const estimatedLoginCount = useMemo(() => {
const { userCount, flowDuration } = estimateInput;
// if (loginCount > 0) return loginCount;
// Assumption that users log in over a period of 15 minutes.
return Math.ceil(userCount / 15.0 / 60.0) * flowDuration;
}, [estimateInput]);
const estimatedLoginValue = estimateInput[FieldName.ConcurrentLogins];
const handleFieldChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {
const { name, value } = event.target;
const nextFieldValue = value.length ? parseInt(value, 10) : -1;
setEstimateInput((currentEstimate) => ({
...currentEstimate,
[name as FieldName]: nextFieldValue,
}));
}, []);
return (
<>
<h3>
<Translate id="ak.scalingCalculator.usageEstimates">Usage Estimates</Translate>
</h3>
<Admonition type="info" icon={null} title={null}>
<form className={styles.admonitionForm} autoComplete="off">
<div className={styles.labelGroup}>
<label htmlFor={FieldID.UserCount}>
<Translate id="ak.scalingCalculator.activeUsersLabel">
Active Users
</Translate>
</label>
<p>
<Translate id="ak.scalingCalculator.activeUsersDescription">
This is used to calculate database storage, and estimate how many
concurrent logins you can expect.
</Translate>
</p>
</div>
<div className={styles.field}>
<input
id={FieldID.UserCount}
type="number"
step="10"
name={FieldName.UserCount}
value={estimateInput[FieldName.UserCount]}
onChange={handleFieldChange}
required
min={1}
/>
</div>
<div className={styles.labelGroup}>
<label htmlFor={FieldID.FlowDuration}>
<Translate id="ak.scalingCalculator.flowDurationLabel">
Flow Duration
</Translate>
</label>
<p>
<Translate id="ak.scalingCalculator.flowDurationDescription">
A single login may take several seconds for the user to enter their
password, MFA method, etc. If you know what usage pattern to expect,
you can override that value from the computed one.
</Translate>
</p>
</div>
<div className={styles.field}>
<input
id={FieldID.FlowDuration}
type="number"
step="5"
name={FieldName.FlowDuration}
value={estimateInput[FieldName.FlowDuration]}
onChange={handleFieldChange}
min={0}
/>
</div>
<div className={styles.labelGroup}>
<label htmlFor={FieldID.ConcurrentLogins}>
<Translate id="ak.scalingCalculator.concurrentLoginsLabel">
Concurrent Logins
</Translate>
</label>
<p>
<Translate id="ak.scalingCalculator.concurrentLoginsDescription">
We estimate that all of the users will log in over a period of 15
minutes, greatly reducing the load on the instance.
</Translate>
</p>
</div>
<div className={styles.field}>
<input
id={FieldID.ConcurrentLogins}
type="number"
step="10"
name={FieldName.ConcurrentLogins}
placeholder={estimatedLoginCount.toString()}
value={estimatedLoginValue === -1 ? "" : estimatedLoginValue.toString()}
onChange={handleFieldChange}
min={0}
/>
</div>
</form>
</Admonition>
<h3>
<Translate id="ak.scalingCalculator.deploymentConfigurations">
Deployment Configurations
</Translate>
</h3>
<SetupComparisionTable {...estimateInput} />
<h3>
<Translate id="ak.scalingCalculator.DatabaseConfigurations">
Database Configurations
</Translate>
</h3>
<DatabaseEstimateTable {...estimateInput} />
</>
);
};
export default ScalingCalculator;

View File

@ -0,0 +1,127 @@
.admonitionForm {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: var(--ifm-global-spacing);
@media (max-width: 1119px) {
grid-template-columns: 1fr;
}
.labelGroup {
color: var(--ifm-alert-color);
label {
font-weight: var(--ifm-font-weight-bold);
}
}
.field {
display: flex;
align-items: center;
position: relative;
input[type="number"] {
font-weight: var(--ifm-font-weight-bold);
font-size: 1.25em;
flex: 1 1 auto;
background-color: var(--ifm-alert-background-color-highlight);
padding: 1em;
border-color: var(--ifm-alert-background-color-highlight);
border-radius: var(--ifm-alert-border-radius);
border-style: double;
box-shadow: inset var(--ifm-global-shadow-lw);
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
}
}
&:has(input[name="flowDuration"])::after {
font-variant: all-petite-caps;
font-weight: var(--ifm-font-weight-bold);
font-size: 1.25em;
flex: 0 0 auto;
content: "sec";
padding: var(--ifm-global-spacing);
display: block;
position: absolute;
right: 0;
color: var(--ifm-color-emphasis-700);
font-family: var(--ifm-font-family-monospace);
}
}
}
.admonitionTable {
padding: 0;
overflow: hidden;
}
.comparisionTable {
display: grid;
grid-template-columns: repeat(var(--ak-comparision-table-columns), 1fr);
overflow-x: auto;
& > header {
border-bottom: 1px solid var(--ifm-alert-border-color);
}
& > header,
& > section {
display: grid;
grid-column: span var(--ak-comparision-table-columns);
grid-template-columns: subgrid;
}
& > section:not(:last-child) {
border-block-end: 1px solid var(--ifm-alert-background-color-highlight);
}
& > section:nth-child(odd) {
/* background-color: var(--ifm-table-stripe-background); */
.rowLabel,
.fieldValue {
background-color: var(--ifm-table-stripe-background);
}
}
.columnLabel {
background-color: var(--ifm-alert-background-color-highlight);
font-weight: var(--ifm-table-head-font-weight);
padding: var(--ifm-table-cell-padding) calc(var(--ifm-spacing-horizontal) * 2);
text-align: center;
font-weight: var(--ifm-font-weight-bold);
place-content: center;
}
.rowLabel {
position: sticky;
left: 0;
z-index: var(--ifm-z-index-dropdown);
backdrop-filter: blur(4px);
overflow: hidden;
border-block-end-width: 4px;
}
.rowLabel,
.fieldValue {
background-color: var(--ifm-alert-background-color);
padding: var(--ifm-table-cell-padding) calc(var(--ifm-spacing-horizontal) * 2);
text-align: center;
font-weight: var(--ifm-font-weight-bold);
place-content: center;
&:not(:last-child) {
border-inline-end: 1px solid var(--ifm-alert-background-color);
}
}
.fieldValue {
font-size: 1.25em;
padding: var(--ifm-table-cell-padding);
place-items: center;
}
}