Last updated: July 2026
Most Keycloak Admin API tutorials stop at “create one user.” At scale you need correct pagination with first and max — the API returns a page, not everything, and there is no total in the list response — so you use the dedicated /count endpoints. You also need a strategy for bulk user creation that avoids one HTTP call per user, and you must know that realm import with tens of thousands of users can take many hours. Authenticate with a service account or admin token, then batch deliberately.
Running Keycloak in production with thousands — or hundreds of thousands — of users is a different problem from the getting-started tutorials. You need to automate user lifecycle management, migrate users from another identity provider, or export data for audits. The Keycloak Admin REST API is the right tool, but its pagination model and bulk-operation options have sharp edges that catch teams off guard. This guide walks through every stage: authentication, paginating large user sets, retrieving accurate totals, bulk creation strategies, and the mitigations for the slow-import problem.
Getting an Admin Token
Every Admin API call requires a valid bearer token. For automation, use a service account: create a confidential client, enable “Service accounts roles,” and assign the realm-management roles your script needs (view-users, manage-users at minimum). For the full setup, see Keycloak machine-to-machine authentication.
TOKEN=$(curl -s -X POST
"https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token"
-H "Content-Type: application/x-urlencoded"
-d "grant_type=client_credentials"
-d "client_id=my-admin-client"
-d "client_secret=MY_CLIENT_SECRET"
| jq -r '.access_token')
Never hard-code credentials — load them from environment variables or a secrets manager. Tokens from a realm-local service account only manage that realm; master-realm admin tokens have cross-realm rights. Prefer realm-local for the principle of least privilege. For guidance on validating the tokens your own APIs receive, see Keycloak token validation for APIs.
Pagination: first and max Done Correctly
The users list endpoint is:
GET /admin/realms/{realm}/users
The two critical query parameters are first (zero-based offset) and max (page size). By default, max is 100 and first is 0. The API does not return a total count in the list response — it returns at most max users starting at first.
A common mistake is calling the endpoint once without parameters and assuming you got all users. You did not. Keycloak silently caps the result at 100 (or whatever max defaults to server-side). If you have 5,000 users, you see 100 and never know you missed 4,900.
The correct approach is to iterate until a page shorter than max:
#!/usr/bin/env bash
# Paginate all users from a Keycloak realm
REALM="myrealm"
BASE_URL="https://keycloak.example.com"
PAGE_SIZE=500
OFFSET=0
ALL_USERS="[]"
while true; do
PAGE=$(curl -s
-H "Authorization: Bearer $TOKEN"
"${BASE_URL}/admin/realms/${REALM}/users?first=${OFFSET}&max=${PAGE_SIZE}")
COUNT=$(echo "$PAGE" | jq 'length')
ALL_USERS=$(echo "$ALL_USERS $PAGE" | jq -s '.[0] + .[1]')
if [ "$COUNT" -lt "$PAGE_SIZE" ]; then
break
fi
OFFSET=$((OFFSET + PAGE_SIZE))
done
echo "$ALL_USERS" | jq 'length'
A page size of 500 is a reasonable default. Using very large values (5,000+) can cause memory pressure on the Keycloak server and slow responses. For most use cases, 200-500 is the sweet spot. The same pattern works in Python or any HTTP client: extend a list, break when the page length is less than max, advance first by max each iteration.
Getting the Total User Count
Because the list endpoint does not return a total field, you need a separate call to get the count:
curl -s
-H "Authorization: Bearer $TOKEN"
"https://keycloak.example.com/admin/realms/myrealm/users/count"
This returns a plain integer. You can use it to pre-allocate storage, show progress bars, or validate that your export matches the expected number of users.
The count endpoint also accepts filter parameters that mirror the list endpoint — so you can count how many users match a given search before fetching them:
# Count enabled users with a verified email
curl -s
-H "Authorization: Bearer $TOKEN"
"https://keycloak.example.com/admin/realms/myrealm/users/count?enabled=true&emailVerified=true"
Always call /users/count before and after a bulk operation to verify correctness.
Search and Filter Parameters
Use query parameters to reduce the dataset before pagination — far more efficient than fetching everything and filtering client-side:
| Parameter | Type | Behavior |
|---|---|---|
search |
string | Prefix-searches username, email, first name, last name |
email / username |
string | Partial match; add exact=true for exact matching |
enabled |
boolean | Filter by account enabled/disabled state |
emailVerified |
boolean | Filter by email verification status |
briefRepresentation |
boolean | Return only id, username, email — much faster for large realms |
briefRepresentation=true is important for bulk operations. A full user representation includes credentials, attributes, and group memberships. For workflows that only need IDs to feed into subsequent calls, brief mode cuts response size by 80-90% and can be the difference between a 10-second page and a 2-second one.
Bulk User Creation: Three Strategies
Creating users one at a time with individual POST /admin/realms/{realm}/users calls works at small scale. At 10,000 users, you are looking at 10,000 HTTP round trips plus individual password-hashing operations — on a typical Keycloak instance that is measured in hours, not minutes.
Strategy 1: Looping POSTs
The straightforward approach: iterate your user list and POST each user. Add retry logic and a brief sleep between calls to avoid overwhelming the server.
import time
import requests
def create_user(session, base_url, realm, user_payload, max_retries=3):
url = f"{base_url}/admin/realms/{realm}/users"
for attempt in range(max_retries):
resp = session.post(url, json=user_payload, timeout=15)
if resp.status_code == 201:
return resp.headers.get("Location")
if resp.status_code == 409:
# User already exists — treat as idempotent success
return None
if resp.status_code == 429 or resp.status_code >= 500:
wait = 2 ** attempt
time.sleep(wait)
continue
resp.raise_for_status()
raise RuntimeError(f"Failed to create user after {max_retries} attempts")
The realistic throughput for individual POSTs with bcrypt hashing is 5-20 users per second on a typical Keycloak instance. For 50,000 users that is 45 minutes at best. Use this strategy for small batches (under 1,000 users) or when you need fine-grained error handling per user.
Strategy 2: Partial Import (POST /admin/realms/{realm}/partialImport)
Partial import is the correct tool for bulk user creation without a full realm teardown. It accepts a JSON body with the same format as a realm export but only processes the sections you include:
curl -s -X POST
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
"https://keycloak.example.com/admin/realms/myrealm/partialImport"
-d '{
"ifResourceExists": "SKIP",
"users": [
{
"username": "alice",
"email": "[email protected]",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "temporary-password",
"temporary": true
}
]
}
]
}'
The ifResourceExists field controls collision behavior: SKIP, OVERWRITE, or FAIL. Use SKIP for idempotent migrations and OVERWRITE when you want to push updated attributes.
Partial import processes users in a single transaction per API call and is significantly faster than looping POSTs because it batches the database writes. Split your user set into batches of 500-2,000 users per request — very large batches increase the risk of a single request timing out and losing all progress.
def bulk_import_users(session, base_url, realm, users, batch_size=1000):
url = f"{base_url}/admin/realms/{realm}/partialImport"
results = {"added": 0, "skipped": 0, "overwritten": 0, "errors": []}
for i in range(0, len(users), batch_size):
batch = users[i : i + batch_size]
payload = {"ifResourceExists": "SKIP", "users": batch}
resp = session.post(url, json=payload, timeout=120)
resp.raise_for_status()
body = resp.json()
results["added"] += body.get("added", 0)
results["skipped"] += body.get("skipped", 0)
results["overwritten"] += body.get("overwritten", 0)
return results
Strategy 3: Full Realm Import at Startup (and Why It Can Take 24 Hours)
Keycloak supports importing a full realm JSON at startup using the --import-realm flag (Keycloak 26.x on Quarkus). This is useful for greenfield deployments — you export your realm config plus users and boot the new instance with the data already loaded.
The problem is password hashing. Each stored credential with a raw password value must be hashed using the configured algorithm (bcrypt or Argon2) before storage. With bcrypt at cost factor 27 (Keycloak’s 26.x default for high-security configs) and 100,000 users, the CPU time is significant — the import can run for 6-24 hours on a single-node instance.
Mitigations:
-
Import pre-hashed credentials. If your source system uses bcrypt or Argon2, export the hashed values and import them directly as
secretDataandcredentialDatafields. Keycloak stores these as-is without rehashing. This is only possible when the algorithm and parameters match. -
Import with temporary passwords or no credentials. Import users with
"temporary": truepasswords or omit credentials entirely and trigger a “reset password” email flow after import. Users reset their own passwords on first login, distributing the hashing cost over time. -
Use partial import in batches during off-peak hours. Rather than a single startup import, use the
partialImportendpoint in batches of 1,000 over several hours during low-traffic periods. -
Scale out temporarily. If you need speed and can hash in parallel, run partial import requests against multiple Keycloak nodes simultaneously — each node hashes its batch independently.
-
Reduce bcrypt work factor for migration only. You can temporarily lower the password policy work factor during import, then raise it again. Users will be re-hashed on next login with the new policy. This is a security trade-off that should be documented and approved.
For a deep dive on realm-level export/import, see Keycloak realm export and import strategy.
Bulk Update and Delete Operations
The Admin API does not expose a single “delete all users matching X” endpoint. Bulk delete requires you to paginate, collect IDs, and issue DELETE /admin/realms/{realm}/users/{id} for each one.
A critical gotcha: when deleting users while iterating pages, do not advance the first offset after each batch. Deleting users shifts the position of remaining records, so the next page at the advanced offset skips users. Re-fetch from first=0 with the same filter each time — slower, but correct.
A common reason to bulk-delete is purging inactive accounts for compliance; see Keycloak and GDPR: deleting, anonymizing, and purging user data for a retention-aware approach.
For bulk attribute updates, use PUT /admin/realms/{realm}/users/{id}. The PUT replaces the entire user representation, not just the changed fields. Always fetch the current user first, merge your changes, then PUT — a blind PUT will silently drop groups, attributes, and role mappings you did not include.
Handling Rate Limiting, Retries, and Backoff
Keycloak does not expose native HTTP rate limiting on the Admin API, but your infrastructure (reverse proxy, WAF, or Skycloak platform controls) may. More commonly you will hit database connection pool exhaustion or CPU saturation under high concurrency.
Key practices:
- Limit concurrency to 5-10 concurrent Admin API requests. Higher concurrency degrades throughput due to connection pool contention.
- Exponential backoff on 429 and 5xx: wait
2^attemptseconds, cap at 60 seconds, max 5 attempts. - Treat 409 Conflict as success on
POST /users— the user already exists, which is fine for migration pipelines. - Track progress to disk. Write completed batch offsets to a file so a crashed script can resume without re-importing finished batches.
For infrastructure-as-code automation, see using Terraform to set up and configure Keycloak and Keycloak Terraform advanced patterns for idempotent provisioning strategies at the provider level.
Quick-Reference: Task, Endpoint, and Scale Tip
| Task | Endpoint | At-Scale Tip |
|---|---|---|
| Get all users | GET /admin/realms/{realm}/users |
Use first/max pagination; never omit max |
| Count users | GET /admin/realms/{realm}/users/count |
Call before and after bulk ops to verify |
| Filter users | GET /admin/realms/{realm}/users?search=&email= |
Add briefRepresentation=true for speed |
| Create single user | POST /admin/realms/{realm}/users |
Use for under 1,000 users; add retry on 429/5xx |
| Bulk create | POST /admin/realms/{realm}/partialImport |
Batch 500-2,000 users per call; use SKIP for idempotency |
| Startup import | --import-realm flag |
Pre-hash credentials or use temporary passwords to avoid 24h hashing |
| Update user | PUT /admin/realms/{realm}/users/{id} |
Fetch first, merge, PUT — never blind-PUT |
| Delete user | DELETE /admin/realms/{realm}/users/{id} |
Paginate IDs first; do not advance offset while deleting |
| Search groups | GET /admin/realms/{realm}/groups |
Same first/max model applies |
| Export realm | GET /admin/realms/{realm}?export=true |
Only available with the legacy export SPI in some configs; prefer partialExport |
Frequently Asked Questions
How do I paginate the Keycloak users endpoint?
Use the first (zero-based offset) and max (page size) query parameters on GET /admin/realms/{realm}/users. Start with first=0&max=500, process the page, then advance first by max. Stop when the returned page has fewer items than max. Never omit max — Keycloak returns a default of 100 users, not all users, so you will silently miss data.
How do I get the total number of users in Keycloak?
Call the dedicated count endpoint: GET /admin/realms/{realm}/users/count. This returns a plain integer — the list endpoint does not include a total. The count endpoint supports the same filter parameters as the list endpoint, so you can count users matching a specific search before paginating.
How do I bulk import users into Keycloak?
Use POST /admin/realms/{realm}/partialImport with a users array and an ifResourceExists policy (SKIP, OVERWRITE, or FAIL). Batch 500-2,000 users per request. Avoid importing raw plaintext passwords at scale — each triggers a bcrypt or Argon2 hash, which at Keycloak 26.x default settings can take hours for large datasets. Import pre-hashed credentials or use "temporary": true passwords instead.
Why is my Keycloak realm import taking 24 hours?
Every imported plaintext password must be hashed before storage. Bcrypt or Argon2 at high work factors takes tens to hundreds of milliseconds per user on a single CPU core. For 100,000 users that adds up to hours. Fix: import pre-hashed credentials, use temporary passwords, or split the load across partialImport batches during off-peak periods.
How do I bulk delete users in Keycloak?
There is no bulk-delete endpoint. Paginate to collect IDs, then DELETE /admin/realms/{realm}/users/{id} for each. Do not advance first between batches while deleting — deleted users shift the window. Re-fetch from first=0 each time to find remaining matches.
Conclusion
Keycloak’s Admin REST API is powerful and largely consistent, but it rewards careful reading of the pagination contract. The key takeaways:
- Always paginate with explicit
firstandmaxparameters — the default cap of 100 users will silently truncate large result sets. - Use
GET /admin/realms/{realm}/users/countfor totals; the list endpoint does not provide them. - Choose partial import over looping POSTs for bulk user creation. Batch to 500-2,000 users per request and use
ifResourceExists: SKIPfor safe idempotency. - Startup realm import with plaintext passwords hashes every credential synchronously — pre-hash or use temporary passwords to avoid multi-hour import times.
- Treat 409 Conflict as success in migration pipelines, implement exponential backoff on 5xx responses, and track progress to disk for crash recovery.
Running Keycloak at scale with confidence is exactly what Skycloak is built for — the platform handles operational concerns so your team focuses on integration logic, not infrastructure. Explore Skycloak plans to see how managed Keycloak simplifies production operations.