Automating Keycloak at Scale: Admin API Pagination and Bulk Operations

Guilliano Molaire Guilliano Molaire 10 min read

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:

  1. Import pre-hashed credentials. If your source system uses bcrypt or Argon2, export the hashed values and import them directly as secretData and credentialData fields. Keycloak stores these as-is without rehashing. This is only possible when the algorithm and parameters match.

  2. Import with temporary passwords or no credentials. Import users with "temporary": true passwords 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.

  3. Use partial import in batches during off-peak hours. Rather than a single startup import, use the partialImport endpoint in batches of 1,000 over several hours during low-traffic periods.

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

  5. 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^attempt seconds, 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 first and max parameters — the default cap of 100 users will silently truncate large result sets.
  • Use GET /admin/realms/{realm}/users/count for 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: SKIP for 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.

Tired of running Keycloak yourself?

Skycloak runs real upstream Keycloak for you with a 99.99% SLA. No fork, no lock-in, just managed Keycloak that stays patched and on call so you don't have to.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman