authentik spends 1.2 seconds of CPU on every password login. Keycloak spends 72 milliseconds on the same job. On a 2 vCPU server that is the difference between 1.8 and 26 logins per second, and no feature table will tell you that.
This Keycloak vs authentik vs Zitadel vs Authelia comparison is built on numbers we measured. Each identity provider ran from the tested Docker Compose stack of its own guide in this series, on the same Ubuntu 26.04 VM, one stack at a time. We timed restarts and password logins, counted CPU per login, ran 10 parallel clients, stepped RAM down until each one broke, and upgraded each from an earlier release with a probe running. The feature comparison next to the numbers was checked against the current documentation of each project, not memory. Every number below comes from Keycloak 26.8.0, authentik 2026.8.3, Zitadel v4.19.4 and Authelia 4.39.28 with LLDAP 0.6.3, tested in October 2026.

The diagram shows what you sign up to run. Keycloak is one big Java process. authentik splits into a web server and a background worker. Zitadel puts its login pages in a separate Next.js container. Authelia needs a directory beside it, and LLDAP is the small one we used. All four keep their state in PostgreSQL.
Keycloak vs authentik vs Zitadel vs Authelia at a glance
| Metric | Keycloak | authentik | Zitadel | Authelia + LLDAP |
|---|---|---|---|---|
| Containers in the stack | 2 | 3 | 3 | 4 |
| Idle memory, all containers | 539 MiB | 715 MiB | 214 MiB | 70 MiB |
Image size, docker compose images | 1.40 GB | 2.37 GB | 0.96 GB | 0.80 GB |
| Restart to ready (data kept) | 17.7 s | 12.9 s | 12.6 s | 14.2 s |
| Password check, median | 38 ms | 471 ms | 946 ms | 353 ms |
| CPU per scripted login | 72 ms | 1,187 ms | 997 ms | 195 ms |
| Logins per second, 10 parallel clients | 26.2 | 1.8 | 2.1 | 5.3 |
| Lowest RAM that passed | 1024 MiB | 1280 MiB | 768 MiB | 768 MiB |
| First RAM step that failed | 768 MiB | 1024 MiB | 512 MiB | 512 MiB |
| Upgrade outage, earlier release to current | 10.7 s | 40.8 s | 6.6 s | 4.1 s |
Lower is better everywhere except logins per second. The upgrade row is how long the health or discovery URL of the login service failed during the upgrade, measured from outside through Nginx.
How we tested
One VM, cloned from the Ubuntu 26.04 template on a Proxmox node with an Intel Core i7-1165G7: 2 vCPU (host CPU type), 4 GB RAM with the balloon fixed at the same size, 40 GB disk, and Docker CE from the Docker repository (29.8.2 with Compose 5.6.0). Only one stack ran at a time, and the others were removed with docker compose down before the next one started. Nginx 1.28 on the host terminated TLS for all four with the same self-signed certificate, using the vhost from each guide. In production each guide uses Let’s Encrypt, and the Nginx with Let’s Encrypt guide for Ubuntu 26.04 covers that part.
Each stack is the core setup from its guide, with the hostnames changed to example.com names: the Keycloak production setup in Docker with one node instead of two, the authentik Docker Compose guide without the LDAP outpost, the Zitadel Docker Compose guide, and the Authelia and LLDAP guide. One test user was created through the API of each product. Password hashing stayed at the default of each product.
| Metric | How it was measured |
|---|---|
| Idle memory | docker stats 10 minutes after the last restart, summed per stack. MemTotal minus MemAvailable inside the guest moved in step: 618 MiB with no stack, then +584, +756, +273 and +160 MiB |
| Restart to ready | docker compose down and up -d with data kept, until every ready URL returned 200 through Nginx. Three runs each, all within 0.1 s, median shown |
| Password check | Wall time of the single HTTP request that verifies the password, 30 logins in a row after one warm-up |
| CPU per login | CPU time of all containers in the stack from cgroup cpu.stat, divided by the 30 logins. It covers every request of the scripted login, listed below |
| Logins per second | 60 complete logins spread over 10 threads, all 60 succeeded on every stack |
| RAM floor | VM memory lowered step by step with a reboot each time. A step passed when the stack came back on its own, 5 logins worked, and 5 more worked 60 seconds later |
| Upgrade outage | A probe requested the health or discovery URL every 0.5 s through Nginx while the upgrade commands from the guide ran. Images were pulled before the stop, as in the guides |
The scripted login is the shortest real path each product offers:
- Keycloak: the authorization request to a realm, then the POST of the login form, which returns the redirect with a code.
- authentik: the default authentication flow through its flow executor API, with the identification stage and then the password stage.
- Zitadel: one call to the Session API with a password check, which is what its login UI does on the server side.
- Authelia: one POST to
/api/firstfactor, which binds to LLDAP with the password of the user.
So the CPU column is a fair price per login, but not an equal number of requests. Keycloak does two requests and authentik four, while Zitadel and Authelia do one each.
Login cost decides how many CPUs you need
The password hash explains almost all of the gap. Each product picks a deliberately slow algorithm, and the CPU cost per login differs by a factor of 16:
| Product | Default password hash | CPU per login | Logins per second on 2 vCPU |
|---|---|---|---|
| Keycloak | Argon2id, 7 MiB memory, 5 iterations | 72 ms | 26.2 |
| authentik | PBKDF2-SHA256, 1,000,000 iterations | 1,187 ms | 1.8 |
| Zitadel | bcrypt, cost 14 | 997 ms | 2.1 |
| Authelia + LLDAP | OPAQUE with Argon2id in LLDAP | 195 ms | 5.3 |
We read the Keycloak and authentik parameters from the stored credentials of the test user:
{"hashIterations":5,"algorithm":"argon2","additionalParameters":{"hashLength":["32"],"memory":["7168"],"type":["id"],"version":["1.3"],"parallelism":["1"]}}
pbkdf2_sha256$1000000
Two cores divided by the CPU per login predicts the throughput, and Keycloak, authentik and Zitadel landed within 6% of that estimate. Authelia reached about half of it, so something other than the CPU of its containers limits it under parallel load. In plain terms, at the measured rates 100 people signing in during the same minute keep authentik busy for about 57 seconds of that minute on this VM, while Keycloak clears them in 4. Under that load an authentik login waited a median of 3.3 seconds and a Zitadel login 4.7 seconds, against 0.33 seconds on Keycloak.
None of this matters for a homelab with ten users who stay signed in for weeks. It matters for a school, a call centre or anything with a morning rush, where it is the number to size CPUs against. Passkey logins skip the password hash, so moving users to passkeys cuts this cost as well. On Authelia that holds only where the access policy is one_factor, because it counts a passkey as one factor and asks for the password on two-factor sites.
Where each one runs out of memory
The RAM floor is the smallest VM that still booted the stack unattended and served logins. Visible memory is what the guest reported after the kernel took its share.
| Product | Last pass (visible to the guest) | First failure | What failed |
|---|---|---|---|
| Keycloak | 1024 MiB (955 MiB) | 768 MiB | The JVM refused to start, 20 restarts: There is insufficient memory for the Java Runtime Environment to continue |
| authentik | 1280 MiB (1206 MiB) | 1024 MiB | Booted and served 5 logins, then the kernel killed a web worker: Out of memory: Killed process 2314 (gunicorn: worke) |
| Zitadel | 768 MiB (703 MiB) | 512 MiB | The kernel killed the login UI: Out of memory: Killed process 1410 (next-server (v1) |
| Authelia + LLDAP | 768 MiB (703 MiB) | 512 MiB | 3 of 4 boots failed, logins returned 502 and the kernel killed Authelia: Out of memory: Killed process 2537 (authelia) |
Keycloak fails for a reason you can see in the JVM error. Our guide sets mem_limit: 1536m and -XX:InitialRAMPercentage=50, so the JVM commits 768 MiB of heap at startup, more than the 703 MiB the VM had:
OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000bcc00000, 805306368, 0) failed; error='Not enough space' (errno=12)
# There is insufficient memory for the Java Runtime Environment to continue.
Lowering the container limit to 512m didn’t rescue it. The JVM started, grew to about 310 MiB resident, and the kernel killed java instead. Keycloak with PostgreSQL wants a 1 GB VM, and on 1 GB it still took 34 seconds from boot to the first served page.
authentik is the only one that failed after it had already worked. At 1024 MiB it came up, answered the first five logins, and lost a gunicorn worker to the OOM killer within the next minute. The allocation that triggered the kill came from Ubuntu’s own package-data downloader, a reminder that the OS shares the same RAM. At 768 MiB the server and worker restarted 15 and 11 times without ever answering. Give it 1.5 GB or more in production.
Zitadel and Authelia both run in 768 MiB with room to spare. At 512 MiB Zitadel lost its Next.js login container to the OOM killer on the first boot. Authelia came up on one of four tries, which is not a floor you can rely on. The memory numbers include Ubuntu, Docker and Nginx, which share the same RAM on a small VPS.
Upgrade downtime
Each stack was installed fresh at an earlier release, given a user, and then upgraded with the commands from its guide while a probe hit the health or discovery URL every half second through Nginx. Images were pulled before the old containers stopped, so the numbers measure the restart and the database migrations, not your download speed.
| Product | From, to | Outage | Failed probe responses | After the upgrade |
|---|---|---|---|---|
| Keycloak, one node | 26.7.5 to 26.8.0 | 10.7 s | 21 x 502 | Login worked, realm and user intact |
| authentik | 2026.5.7 to 2026.8.3 | 40.8 s | 46 x 503, 33 x 502, 1 x 500 | Login worked |
| Zitadel | v4.18.0 to v4.19.4 | 6.6 s for the login UI, 2.1 s for the API | 13 x 502 on the login UI, 4 x 502 on the API | Login worked |
| Authelia + LLDAP | 4.39.15 and 0.6.2 to 4.39.28 and 0.6.3 | 4.1 s | 8 x 502 | Login worked |
The long authentik window is its migrations. The server answers 503 while it migrates and 502 while the container is replaced, and a single server cannot do a rolling upgrade, so this is a maintenance window. It also refuses to skip a release line, which its guide covers. Keycloak 26.7 to 26.8 cannot roll either, because the update compatibility check flags the new login failure store and stops at exit code 3. With two nodes, as in the Keycloak guide, the same upgrade measured 19 seconds on the lab host of that guide. Patch releases that pass the check roll one node at a time with no outage at all.
Zitadel replaces two containers. On a second run we probed both: the API was gone for 2.1 seconds (1.5 seconds on the first run), while the Next.js login UI took 6.6 seconds to come back, and users cannot sign in without it. Authelia was back in 4.1 seconds, close to the 4 seconds measured in its own guide. A release with a heavy migration takes longer on any of the four, so read the release notes before you pull.
Feature comparison
On a fresh install, the four login pages look like this. Only Zitadel offers self-registration on a fresh install. The passkey button on the Authelia page comes from the passkey login our guide switches on:

Every row was checked against the documentation or source of each project at the tested release on 6 October 2026.
| Feature | Keycloak | authentik | Zitadel | Authelia + LLDAP |
|---|---|---|---|---|
| License | Apache-2.0 | MIT, enterprise directory under a commercial license | AGPL-3.0 since v3 | Apache-2.0 (LLDAP GPL-3.0) |
| OpenID Connect provider | Yes | Yes | Yes | Yes, still labelled beta |
| SAML 2.0 provider | Yes | Yes | Yes | No, on the roadmap as planning |
| LDAP interface for old apps | No | Yes, LDAP outpost | No | Yes, LLDAP is the LDAP server |
| RADIUS | No | Yes, EAP-TLS needs Enterprise | No | No |
| Forward auth for apps without SSO | No, add oauth2-proxy | Yes, proxy provider | No, add oauth2-proxy | Yes, its main job |
| Passkeys | Yes | Yes | Yes | Yes, counts as one factor |
| TOTP | Yes | Yes | Yes | Yes |
| SCIM | Inbound, supported since 26.8 | Inbound and outbound | Inbound, preview, users only | No |
| Multi-tenancy | Realms, plus organizations | Brands; real tenants are Enterprise and alpha | Instances and organizations | No |
| Admin web UI | Yes | Yes | Yes | No, users live in the LLDAP UI |
| Self-registration | Yes | Yes | Yes | No |
| Social and external IdP login | Yes | Yes, the in-flow source stage is Enterprise | Yes | No |
| LDAP or AD as a user source | Yes, with sync | Yes, with sync | Yes, checked at login, no bulk sync | Yes, it is the backend |
| Config as code | Realm import, Terraform | Blueprints, Terraform | Terraform | One YAML file |
| Kubernetes | Operator | Helm chart | Helm chart | Helm chart, beta |
| Databases | PostgreSQL, MariaDB, MySQL, Oracle, SQL Server | PostgreSQL only | PostgreSQL only | PostgreSQL, MySQL, SQLite |
| Runtime | Java on Quarkus | Python with Rust and Go parts | Go, Next.js login | Go; LLDAP in Rust |
A few rows deserve a closer look before you choose:
- authentik dropped Redis in 2025.10. A current authentik stack is PostgreSQL only, and many older compose files and tutorials still start a Redis container it no longer uses.
- Keycloak 26.8 promoted its inbound SCIM API from preview to supported, so an HR system can now push users into a realm without an extension.
- Zitadel moved from Apache-2.0 to AGPL-3.0 with v3. Running it unmodified is unaffected, but offering a modified build as a network service means publishing your changes.
- The Authelia OpenID Connect provider works and we used it for Grafana in its guide, but the project still calls it beta and warns about occasional breaking changes.
Keycloak
The heavyweight that is cheapest per login. It idles at 539 MiB, the JVM takes about 33 seconds from VM boot to the first served page, and it needs a 1 GB VM at minimum. In exchange it has the broadest protocol coverage here, the most database choices and realms that fully separate tenants. The 72 ms login cost let two cores handle about 1,500 sign-ins per minute in our test.
Keycloak protects only applications that speak OIDC or SAML. A static site or an old admin panel needs oauth2-proxy in front. The admin console puts every realm setting a click away, which is powerful and takes a while to learn. Until you replace the bootstrap admin, it also nags you with a yellow banner:

The Keycloak guide runs two nodes behind Nginx, which doubles the JVM memory and buys zero-downtime restarts.
authentik
The most features in one box, and the most expensive login. A forward-auth proxy, an LDAP server, RADIUS, SCIM in both directions and visual flows all ship in the same image, and that image is the largest here at 1.95 GB. It also used the most memory at idle, 715 MiB across three containers.

The default PBKDF2 setting of 1,000,000 iterations is what costs 1.2 seconds of CPU per login. That is the Django default, and authentik doesn’t lower it. The first start ran every migration and took 84 seconds before the server answered. An upgrade is the slowest here too, at 40.8 seconds of outage. Upgrades must go one release line at a time, which the server enforces at startup.
Zitadel
The leanest of the three full identity platforms: 214 MiB at idle, 0.96 GB of images, a first start of 8 seconds once PostgreSQL was up, an upgrade that kept the login page away for 6.6 seconds and a RAM floor of 768 MiB. It has organizations and instances for real multi-tenancy, a Session API that makes custom login pages practical, and SAML and OIDC both. The cost is bcrypt at cost 14, close to a second of CPU per password login, which puts it next to authentik in the throughput test.

Its defaults need work before real users arrive. The login page offers Register new user by default, and there is no account lockout until you set one. The Zitadel guide closes both. AGPL-3.0 is the licence to check with your legal team if you plan to modify it.
Authelia with LLDAP
The smallest stack by a wide margin: 70 MiB for four containers, 0.80 GB of images and a 768 MiB floor. Authelia is a gatekeeper rather than a full identity platform. It answers the Nginx auth_request for every request, adds TOTP and passkeys, and can act as an OIDC provider for apps like Grafana. LLDAP holds the users and gives you a basic web UI for them:

There is no admin console for Authelia itself. Access rules, clients and policies live in one YAML file, and changing them means a restart. It has no SAML, no self-registration and no social login. For a homelab or a small team that wants MFA in front of a handful of web apps, those gaps rarely matter and the footprint does.

Which one to run for each job
- Apps that only speak SAML, many databases to choose from, or 1,500 logins per minute on two cores: Keycloak. Its 72 ms login is the cheapest here by a factor of almost three.
- One box that also has to be an LDAP server, a RADIUS server and a forward-auth proxy, with flows built in a UI: authentik. Budget about 1.2 cores per login per second at peak, 1.5 GB of RAM or more, and a maintenance window for upgrades.
- Customer-facing logins, separate organizations per client, or a custom login page on an API: Zitadel, at 214 MiB idle.
- MFA in front of self-hosted web apps on a small VPS or a Raspberry Pi class host: Authelia with LLDAP, at 70 MiB and three times the login rate of authentik on the same cores.