Cognito is regional, users are not!
Step-by-step guide to setup Cognito for cross-region resilience
Cognito is regional, users are not!
Step-by-step guide to setup Cognito for cross-region resilience

I still remember, customer asked me during a disaster recovery review:
“What happens to our Cognito application users if
us-east-1goes down?"
At first, I thought it will be same like other AWS service like Aurora Global Database, DynamoDB Global Tables or S3 Cross-Region Replication. However, after I researched, I learned Cognito DR approaches comes with painful tradeoffs like password resets, complex user synchronization, or user re-login that are difficult to operate in practice.
This post walks through the real customer challenge I encountered in the past, why common disaster recovery patterns fail with Amazon Cognito User Pools, and AWS-native approach we used to achieve regional resilience minimising the need for users to reset their login passwords.
Customer challenge
Customer platform has multiple application users, all authenticated through Amazon Cognito User Pools in us-east-1. The platform carries a 24x7 availability commitment. For Cognito DR, I discovered that Cognito User Pools are strictly regional, see AWS Document . There is no AWS native cross-region replication. The DR region has no user pool. If us-east-1 goes down, users cannot authenticate , not because the application is unavailable, but because the identity service has no presence in the DR region. I proposed to create a Cognito User Pool in us-west-2 and replicate users to it. This is one of the possible approach and bit complicated.
Main Constraints
Cognito has below service constraints:
- **Passwords are never exportable**
- **JWT tokens are pool-specific**
- **Refresh tokens are pool-bound**
- **App client secrets are AWS-generated**
The practical implication is simple:
Users will need to re-authenticate after failover.
The goal is not to eliminate re-login completely, but to minimise the need for users to reset their passwords during an outage.
Evaluating DR options
Once the Cognito limitations became clear, I started evaluating different DR approaches with the help of Kiro. Each option solved part of the problem, but every solution introduced tradeoffs around complexity, cost, operational overhead, or user experience during failover.
Option 1: Backup and restore
One of simplest approach was exporting users to CSV and importing them into a DR pool during failover. In practice, it immediately ran into Cognito’s biggest limitation i.e. passwords are not exportable. Imported users land in [RESET_REQUIRED](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminResetUserPassword.html) state, which means every user must reset their password after failover.
Option 2: Just-in-Time (JIT) migration
If user doesn’t exist in Cognito user pool, AWS provides a Migrate User Lambda trigger that can creates the users. For more details, refer Importing users with a user migration Lambda trigger. From the end user’s perspective, the experience is seamless. No password reset, no migration awareness. The problem is dependency on the primary region. If us-east-1 is completely unavailable, the Lambda cannot validate credentials against the primary pool, and authentication fails. This works well for partial degradation scenarios, but not for full regional outages.
Option 3: OIDC federation
Another approach was federating the DR pool back to the primary pool using OIDC. Refer OIDC user pool IdP authentication flow for more details about authentication flow. Technically, this works. Architecturally, it defeats the purpose of DR entirely. If the primary pool is unavailable, the DR pool still cannot authenticate users. It also adds unnecessary cross-region latency during normal operations so I ruled this option out.
Option 4: Third-Party identity provider
The cleanest long-term solution is moving identity outside Cognito entirely using providers like Okta or Auth0. This solves many of the regional limitations because authentication becomes globally managed outside AWS regions. Architecturally, this is probably the best answer for platforms requiring true global identity resilience.
But the tradeoffs are real:
- additional licensing cost,
- migration effort,
- operational overhead,
- vendor dependency,
- and longer implementation timelines.
For teams needing a practical DR solution within AWS, this is often too large a shift initially.
Option 5: AWS Cognito Export Reference Architecture
AWS also provides a reference architecture that exports Cognito user profiles into DynamoDB Global Tables using Lambda and Step Functions. This improves user attribute availability in the DR region, but it still does not solve the core problem: passwords are not exported. Which means users still need to reset passwords after failover. Useful for profile replication. Not sufficient for seamless authentication recovery.
Option 6: Pre-Sync + JIT Migration
This ended up being the most practical AWS-native approach. The solution combines two patterns:
- Periodic pre-sync of users into the DR pool
- JIT migration as a fallback mechanism.
A scheduled Lambda periodically copies users from the primary pool into the DR pool and pre-creates them in CONFIRMED state using a placeholder password. This ensures the DR pool already contains most user records before failover happens.
For users created after the last sync, Cognito falls back to the Migration Lambda. When those users attempt login, the Lambda validates credentials against the primary pool and migrates them automatically. This approach does not eliminate every limitation, but it provides the best balance between operational simplicity, cost, recovery speed and user experience. Most importantly, it minimises the need for users to reset their passwords during a regional outage.
Comparing the Options
Based on my comparison below, I think Option 6 is the only approach that is AWS-native, requires no application code changes, costs approximately $5 per month, and allows users to log in with their existing passwords after failover when the primary region is at least partially reachable, and for users previously JIT-migrated during DR drills.

The Architecture in Detail
The full solution has four components: the primary Cognito pool in us-east-1, the DR pool in us-west-2, the Pre-Sync Lambda that runs hourly, and the Migration Lambda attached to the DR pool as a trigger.

- Users send login requests over HTTPS. Route 53 resolves the domain and routes traffic to the application in the healthy primary region.
- The application authenticates the user by calling the Cognito Primary User Pool. Cognito verifies credentials and returns JWT tokens to establish the session.
- Every hour, EventBridge Scheduler triggers the Pre-Sync Lambda in the DR region. The Lambda reads pool credentials from the local Secrets Manage replica, fetches the full user roster from the primary Cognito pool, and pre-creates each user in the DR pool with confirmed status ensuring the DR pool is ready before any failover occurs.
- During a regional failure, Route 53 fails over traffic to the DR application. For users not in the DR pool, the Migration Lambda verifies their password against the primary pool and migrates them just-in-time. Pre-synced user records serve as a roster; actual authentication requires the failover runbook to purge placeholder users so JIT migration can handle them.
Note : If the primary region is completely unreachable AND the user was created in the last hour (after the last pre-sync), that specific user cannot log in until primary recovers or they do a password reset. This is a very narrow window.
Implementation
The complete implementation is available in the amazon-cognito-dr GitHub repository with step-by-step setup instructions. Here I cover the key design decisions.
Caveat
I realised, there is one Cognito limitation that cannot be fully avoided during regional failover: active user sessions will eventually require a one-time re-login. The reason is simple. JWT tokens issued by the primary Cognito pool are signed using that region’s RSA keys. The DR pool uses different signing keys, so it cannot validate tokens issued by the primary region.
In practice, once the existing token expires, the application redirects the user to the DR pool login page. The user signs in again using the same credentials and receives a new token from the DR region. This is not a failure of the DR design. It is a fundamental property of JWT-based authentication across independent identity systems.
Testing the DR Scenarios
A DR strategy that has not been tested is not a DR strategy. The following scenarios should be validated in a POC environment before production implementation.
Full setup instructions: See the Setup Guide in the GitHub repository.
Test 0 : Verify Infrastructure State — Before running any DR scenario, confirm the full stack is wired correctly.
Primary pool users (all CONFIRMED):

DR pool empty before sync:

Side-by-side comparison — Primary has users, DR is empty:

EventBridge schedule active:

Full testing instructions: See the Testing Guide in the GitHub repository.
Test 1 : Baseline login to primary pool. Confirm that authentication works normally before testing any DR scenarios. This establishes the expected token format and response structure.
aws cognito-idp admin-initiate-auth \
--user-pool-id "$PRIMARY_POOL_ID" \
--client-id "$PRIMARY_CLIENT_ID_CLI" \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=alice@example.com,PASSWORD='PermanentPass@2026!' \
--region us-east-1 \
--query 'AuthenticationResult.{AccessToken:AccessToken,ExpiresIn:ExpiresIn,TokenType:TokenType}'

Test 2 : Pre-Sync Lambda: Populate DR Pool — The Pre-Sync Lambda is the proactive half of the solution. It runs hourly via EventBridge and copies user records from the primary pool to the DR pool with a placeholder password. This ensures the DR pool has a complete user roster before any failover.
aws lambda invoke \
--function-name cognito-presync \
--payload '{}' \
--cli-binary-format raw-in-base64-out \
--region us-west-2 \
/tmp/presync-output.json && cat /tmp/presync-output.json

DR pool after sync : all users now CONFIRMED:

Test 3 : JIT migration with primary available — Remove a user from the DR pool. Attempt login to the DR pool. The Migration Lambda should fire, verify credentials against the primary pool, and return a token. Check CloudWatch Logs for the migration event. Verify the user now exists in the DR pool.
Step 1 — Remove alice from DR pool to simulate a user not yet synced:
aws cognito-idp admin-delete-user \
--user-pool-id "$DR_POOL_ID" --username alice@example.com --region us-west-2
aws cognito-idp admin-get-user \
--user-pool-id "$DR_POOL_ID" --username alice@example.com --region us-west-2

Step 2 — Login to DR pool — Migration Lambda fires automatically
aws cognito-idp admin-initiate-auth \
--user-pool-id "$DR_POOL_ID" \
--client-id "$DR_CLIENT_ID_CLI" \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=alice@example.com,PASSWORD='PermanentPass@2026!' \
--region us-west-2 \
--query 'AuthenticationResult.{AccessToken:AccessToken,ExpiresIn:ExpiresIn}'

Step 3 — Verify alice now exists in DR pool (created by Migration Lambda)
aws cognito-idp admin-get-user \
--user-pool-id "$DR_POOL_ID" --username alice@example.com --region us-west-2 \
--query '{Username:Username,Status:UserStatus,Attributes:UserAttributes}'

Test 4 : Pre-synced user login (Limitation) — Run the Pre-Sync Lambda manually. Verify users appear in the DR pool with CONFIRMED status. Attempt login with the real password. This will fail with NotAuthorizedException because the DR pool has the placeholder password and the Migration Lambda is not invoked for existing users. This is expected behaviour and demonstrates the limitation.
Step 1 — Confirm bob exists in DR pool (pre-synced with placeholder password):
aws cognito-idp admin-get-user \
--user-pool-id "$DR_POOL_ID" --username bob@example.com --region us-west-2 \
--query '{Username:Username,Status:UserStatus}'

Step 2 — Login with real password — FAILS (expected):
aws cognito-idp admin-initiate-auth \
--user-pool-id "$DR_POOL_ID" --client-id "$DR_CLIENT_ID_CLI" \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=bob@example.com,PASSWORD='PermanentPass@2026!' \
--region us-west-2

This is the documented limitation. Bob exists with placeholder password → mismatch → Migration Lambda NOT invoked.
Step 3 — Workaround: delete bob, then JIT migration handles it:
aws cognito-idp admin-delete-user \
--user-pool-id "$DR_POOL_ID" --username bob@example.com --region us-west-2
aws cognito-idp admin-initiate-auth \
--user-pool-id "$DR_POOL_ID" --client-id "$DR_CLIENT_ID_CLI" \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=bob@example.com,PASSWORD='PermanentPass@2026!' \
--region us-west-2 --query 'AuthenticationResult.AccessToken'

Design implication: The pre-sync value is ensuring user records exist for audit/roster purposes. For actual login during failover, a runbook step can bulk-delete pre-synced users to force JIT migration, or the sync can be enhanced to store real passwords captured during normal login flows.
Test 5 : Simulate Primary Region Outage — Demonstrates what happens during a complete us-east-1 outage. We point the Migration Lambda at an invalid pool ID to simulate the primary being unreachable. This honestly shows both failure modes.
Step 1 — Break the Migration Lambda (simulate primary unreachable):
aws lambda update-function-configuration \
--function-name cognito-migration-trigger \
--environment "Variables={PRIMARY_REGION=us-east-1,PRIMARY_USER_POOL_ID=us-east-1_INVALID,PRIMARY_CLIENT_ID=$PRIMARY_CLIENT_ID_CLI}" \
--region us-west-2 --query 'LastUpdateStatus'

Step 2 — User exists in DR (placeholder password) — login fails at password check:
aws cognito-idp admin-get-user \
--user-pool-id "$DR_POOL_ID" --username charlie@example.com --region us-west-2
aws cognito-idp admin-initiate-auth \
--user-pool-id "$DR_POOL_ID" --client-id "$DR_CLIENT_ID_CLI" \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=charlie@example.com,PASSWORD='PermanentPass@2026!' \
--region us-west-2

Charlie exists with placeholder password → password mismatch → Lambda never invoked. This is the same limitation as Test 4.
Step 3 — Delete charlie, then try — Lambda fires but primary unreachable:
aws cognito-idp admin-delete-user \
--user-pool-id "$DR_POOL_ID" --username charlie@example.com --region us-west-2
aws cognito-idp admin-initiate-auth \
--user-pool-id "$DR_POOL_ID" --client-id "$DR_CLIENT_ID_CLI" \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=charlie@example.com,PASSWORD='PermanentPass@2026!' \
--region us-west-2

The Lambda was invoked (user didn’t exist), attempted to reach the primary pool, failed, and raised “Migration failed”. Cognito surfaced this as UserNotFoundException: UserMigration failed.
Note : After this test, I immediately restored the Lambda to the correct configuration before continuing.
Test 6 : Token portability — Obtain a refresh token from the primary pool. Attempt to use it against the DR pool. It will fail with NotAuthorizedException. Then perform a fresh login to the DR pool with the same credentials. This succeeds and demonstrates that re-login works correctly after failover.
Step 1 — Get tokens from primary pool:
PRIMARY_AUTH=$(aws cognito-idp admin-initiate-auth \
--user-pool-id "$PRIMARY_POOL_ID" --client-id "$PRIMARY_CLIENT_ID_CLI" \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=alice@example.com,PASSWORD='PermanentPass@2026!' \
--region us-east-1)
echo "$PRIMARY_AUTH" | python3 -c "
import sys, json
r = json.load(sys.stdin)['AuthenticationResult']
print('AccessToken (first 50 chars):', r['AccessToken'][:50])
print('RefreshToken (first 50 chars):', r['RefreshToken'][:50])
print('ExpiresIn:', r['ExpiresIn'])
"

Step 2 — Extract refresh token:
PRIMARY_REFRESH=$(echo "$PRIMARY_AUTH" | python3 -c \
"import sys,json; print(json.load(sys.stdin)['AuthenticationResult']['RefreshToken'])")
echo "Refresh token captured (length: ${#PRIMARY_REFRESH})"

Step 3 — Use primary refresh token against DR pool — FAILS:
aws cognito-idp initiate-auth \
--client-id "$DR_CLIENT_ID_CLI" \
--auth-flow REFRESH_TOKEN_AUTH \
--auth-parameters REFRESH_TOKEN="$PRIMARY_REFRESH" \
--region us-west-2

Note : This is expected and unavoidable. The primary pool’s RSA private key signed this token. The DR pool has no knowledge of that key.
Step 4 — Fresh login to DR pool works (re-login after failover):
aws cognito-idp admin-initiate-auth \
--user-pool-id "$DR_POOL_ID" --client-id "$DR_CLIENT_ID_CLI" \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=alice@example.com,PASSWORD='PermanentPass@2026!' \
--region us-west-2 \
--query 'AuthenticationResult.{AccessToken:AccessToken,ExpiresIn:ExpiresIn}'

Test 7 : New user RPO gap — Create a new user in the primary pool without running the Pre-Sync Lambda. Attempt login to the DR pool. The Migration Lambda covers the gap — the user is migrated just-in-time. This demonstrates that the hourly sync interval does not create a hard failure window as long as the primary pool is reachable.
Step 1 — Create ‘dave’ in primary pool (simulates user registered after last sync):
aws cognito-idp admin-create-user \
--user-pool-id "$PRIMARY_POOL_ID" --username dave@example.com \
--user-attributes Name=email,Value=dave@example.com Name=email_verified,Value=true \
--message-action SUPPRESS --temporary-password 'TempPass1@123' \
--region us-east-1 --query 'User.{Username:Username,Status:UserStatus}'
aws cognito-idp admin-set-user-password \
--user-pool-id "$PRIMARY_POOL_ID" --username dave@example.com \
--password 'PermanentPass@2026!' --permanent --region us-east-1

Step 2 — Confirm dave does NOT exist in DR pool:
aws cognito-idp admin-get-user \
--user-pool-id "$DR_POOL_ID" --username dave@example.com --region us-west-2

Step 3 — Dave logs in to DR pool — JIT migration covers the sync gap:
aws cognito-idp admin-initiate-auth \
--user-pool-id "$DR_POOL_ID" --client-id "$DR_CLIENT_ID_CLI" \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=dave@example.com,PASSWORD='PermanentPass@2026!' \
--region us-west-2 --query 'AuthenticationResult.AccessToken'

Step 4 — Cleanup:

Summary
Cognito User Pools are regional with hard constraints like passwords aren’t exportable, tokens are pool-specific, and refresh tokens are pool-bound. Cross-region DR must work around these limits, not against them.
Pre-Migration Sync + JIT Lambda is the most practical AWS-native DR approach considering ~$5/month, no app code changes, and users keep their passwords after a regional outage. The only gap is users created during the last sync interval if the primary is simultaneously unreachable. One unavoidable trade-off: a single re-login at failover.
For immediate resilience within AWS-native Cognito, recommended option offers the best cost-protection-speed balance. For long-term architectural cleanliness, a third-party IdP remains the ideal target.
This blog is refined and improved with the help of Kiro 🤖
메타데이터
- post_id
- 7d110bafc62a
- slug
- cognito-is-regional-your-users-are-not-7d110bafc62a
- url
- https://medium.com/@dr-rahul-gaikwad/cognito-is-regional-your-users-are-not-7d110bafc62a
- canonical_url
- https://medium.com/@dr-rahul-gaikwad/cognito-is-regional-your-users-are-not-7d110bafc62a
- author_url
- https://medium.com/@dr-rahul-gaikwad
- status
- ok
- fetched_at
- 2026-06-09 15:37:30