{
  "schemaVersion": "1.0",
  "code": "50196",
  "name": "LoopDetected",
  "title": "AADSTS50196: App is stuck in a sign-in loop (fix)",
  "url": "https://brambrinkmeier.com/entra/aadsts/50196/",
  "alternates": {
    "markdown": "https://brambrinkmeier.com/entra/aadsts/50196.md",
    "json": "https://brambrinkmeier.com/api/aadsts/50196.json",
    "html": "https://brambrinkmeier.com/entra/aadsts/50196/"
  },
  "description": "AADSTS50196: Entra detected a client loop, the app repeats the same token request. Usually missing token caching. Fix for apps and KQL.",
  "summary": "AADSTS50196 means Entra detected a client loop: the app sent the same request too many times in a short period. Microsoft says the usual cause is missing or broken token caching. It points to a faulty app, not usually to an attacker. The fix is in the app: cache tokens, call the silent method first, and stop retrying on errors.",
  "inMicrosoftReference": true,
  "unlistedChecked": null,
  "meaningText": "Microsoft's description:\n\n> **LoopDetected**: A client loop has been detected. Check the app’s logic to ensure that token caching is implemented, and that error conditions are handled correctly. The app has made too many of the same request in too short a period, indicating that it is in a faulty state or is abusively requesting tokens.\n\nEntra is throttling one client. The app asked for the same token over and over, and Entra stopped answering it with tokens.\n\nMicrosoft's MSAL.NET throttling page says when this happens: *\"Microsoft Entra ID throttles applications when you call the authentication API too frequently. Most often this happens when token caching is not used.\"* It also says how it shows up. For user-facing calls, the app gets an `invalid_grant` error with the message *\"AADSTS50196: The server terminated an operation because it encountered a loop while processing a request\"*. For the client credentials grant (app-only tokens), Entra replies with `429 Too Many Requests` and a `Retry-After: 60` header instead. Our reading: an app-only loop may therefore not show up as 50196 at all, but as throttling with no AADSTS code in the client.",
  "sections": [
    {
      "heading": "What does AADSTS50196 mean?",
      "text": "Microsoft's description:\n\n> **LoopDetected**: A client loop has been detected. Check the app’s logic to ensure that token caching is implemented, and that error conditions are handled correctly. The app has made too many of the same request in too short a period, indicating that it is in a faulty state or is abusively requesting tokens.\n\nEntra is throttling one client. The app asked for the same token over and over, and Entra stopped answering it with tokens.\n\nMicrosoft's MSAL.NET throttling page says when this happens: *\"Microsoft Entra ID throttles applications when you call the authentication API too frequently. Most often this happens when token caching is not used.\"* It also says how it shows up. For user-facing calls, the app gets an `invalid_grant` error with the message *\"AADSTS50196: The server terminated an operation because it encountered a loop while processing a request\"*. For the client credentials grant (app-only tokens), Entra replies with `429 Too Many Requests` and a `Retry-After: 60` header instead. Our reading: an app-only loop may therefore not show up as 50196 at all, but as throttling with no AADSTS code in the client."
    },
    {
      "heading": "Is it an attack?",
      "text": "Usually not. Microsoft's description allows two readings: the app *\"is in a faulty state or is abusively requesting tokens.\"* In practice the first is far more common, and the cause sits in the app's code.\n\n| Pattern | Likely meaning |\n|---|---|\n| One app, many users, starting on the day of a release | A bug in the new version: token cache broken, or a retry loop. |\n| One user, one app, one device | That device's token cache is broken or the app is stuck after an error. Often a browser tab or desktop client stuck in a redirect loop. |\n| A script or tool (PowerShell, CLI, a sync job) | The script asks for a new token on every call instead of reusing one. |\n| One IP, many users, an app you don't recognise | Worth a look, though Microsoft doesn't describe this case. It could be an automated tool replaying tokens at scale. Check what else that IP did. |\n\nMost likely, 50196 is a symptom of request volume, not of a credential being guessed: the user was already signed in."
    },
    {
      "heading": "How do I find it in my logs (KQL)?",
      "text": "Microsoft doesn't document which sign-in table 50196 is logged in. Check where it lands in your workspace:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"50196\"\n| summarize Rows = count(), Apps = dcount(AppId) by SourceTable\n```\n\nWhich apps, users and clients loop:\n\n```kusto\nunion SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"50196\"\n| summarize Rows = count(),\n            Users = dcount(UserPrincipalName),\n            IPs = dcount(IPAddress),\n            Agents = make_set(UserAgent, 5),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated)\n          by AppDisplayName, AppId, ResourceDisplayName\n| sort by Rows desc\n```\n\nA new looping app or a sudden spike. This compares the last day with the 14 days before it:\n\n```kusto\nlet recent =\n    union SigninLogs, AADNonInteractiveUserSignInLogs\n    | where TimeGenerated > ago(1d)\n    | where ResultType == \"50196\"\n    | summarize RecentRows = count(), RecentUsers = dcount(UserPrincipalName) by AppId, AppDisplayName;\nlet baseline =\n    union SigninLogs, AADNonInteractiveUserSignInLogs\n    | where TimeGenerated between (ago(15d) .. ago(1d))\n    | where ResultType == \"50196\"\n    | summarize BaselineRows = count() by AppId;\nrecent\n| join kind=leftouter baseline on AppId\n| extend BaselineRows = coalesce(BaselineRows, 0)\n| where BaselineRows == 0 or RecentRows > 3 * (BaselineRows / 14.0)\n| project AppDisplayName, AppId, RecentRows, RecentUsers, BaselineRows\n| sort by RecentRows desc\n```\n\nThe factor of 3 is a starting point. Non-interactive rows are aggregated when application, user, IP address, status and resource ID match, so a single row can stand for many requests. Count rows to compare apps, not to count requests."
    },
    {
      "heading": "How do I fix AADSTS50196?",
      "text": "**For the user:** close the app or browser tab completely and open it again. If that doesn't help, sign out of the app, clear its cached sign-in, and sign in again. If it keeps happening, report it to whoever runs the app; the fix is in the app.\n\n**For developers:** Microsoft's throttling page lists the usual causes:\n1. *\"Token caching is not setup correctly.\"*\n2. Not calling the silent method (`AcquireTokenSilent`) before an interactive or username/password call.\n3. Asking for a scope that doesn't apply to personal Microsoft accounts, such as `User.ReadBasic.All`, *\"resulting in cache misses.\"*\n\nReuse tokens until they are close to expiry. Don't retry in a tight loop when a call fails. For app-only tokens, honour the `Retry-After` header. MSAL throws `MsalThrottledServiceException` or `MsalThrottledUiRequiredException` when it throttles on the client side; the page notes that *\"If MSAL would not apply client-side throttling the application would still not be able to acquire tokens.\"*\n\n**For admins:**\n1. Use the second query to find the app, then give its owner the time window and the user agents.\n2. If it is a script running under a user account, check that it gets one token and reuses it.\n3. If an unknown app or a single IP produces 50196 for many users, look at that IP's other sign-ins before you close it as a bug."
    }
  ],
  "microsoftWording": {
    "text": "**LoopDetected**: A client loop has been detected. Check the app’s logic to ensure that token caching is implemented, and that error conditions are handled correctly. The app has made too many of the same request in too short a period, indicating that it is in a faulty state or is abusively requesting tokens.",
    "source": "Microsoft's error code reference, as quoted on the page",
    "sourceUrl": "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes"
  },
  "securityReading": {
    "category": {
      "id": "token",
      "label": "Tokens and sessions"
    },
    "kind": {
      "id": "failure",
      "label": "Failure"
    },
    "attackSignal": {
      "level": "low",
      "label": "Low: usually configuration or user error"
    },
    "mitre": [],
    "tables": [
      "SigninLogs",
      "AADNonInteractiveUserSignInLogs",
      "AADServicePrincipalSignInLogs"
    ],
    "isItAnAttack": "Usually not. Microsoft's description allows two readings: the app *\"is in a faulty state or is abusively requesting tokens.\"* In practice the first is far more common, and the cause sits in the app's code.\n\n| Pattern | Likely meaning |\n|---|---|\n| One app, many users, starting on the day of a release | A bug in the new version: token cache broken, or a retry loop. |\n| One user, one app, one device | That device's token cache is broken or the app is stuck after an error. Often a browser tab or desktop client stuck in a redirect loop. |\n| A script or tool (PowerShell, CLI, a sync job) | The script asks for a new token on every call instead of reusing one. |\n| One IP, many users, an app you don't recognise | Worth a look, though Microsoft doesn't describe this case. It could be an automated tool replaying tokens at scale. Check what else that IP did. |\n\nMost likely, 50196 is a symptom of request volume, not of a credential being guessed: the user was already signed in.",
    "conditionalAccess": null
  },
  "kql": [
    {
      "title": null,
      "leadIn": "Microsoft doesn't document which sign-in table 50196 is logged in. Check where it lands in your workspace:",
      "query": "union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"50196\"\n| summarize Rows = count(), Apps = dcount(AppId) by SourceTable"
    },
    {
      "title": null,
      "leadIn": "Which apps, users and clients loop:",
      "query": "union SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"50196\"\n| summarize Rows = count(),\n            Users = dcount(UserPrincipalName),\n            IPs = dcount(IPAddress),\n            Agents = make_set(UserAgent, 5),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated)\n          by AppDisplayName, AppId, ResourceDisplayName\n| sort by Rows desc"
    },
    {
      "title": null,
      "leadIn": "A new looping app or a sudden spike. This compares the last day with the 14 days before it:",
      "query": "let recent =\n    union SigninLogs, AADNonInteractiveUserSignInLogs\n    | where TimeGenerated > ago(1d)\n    | where ResultType == \"50196\"\n    | summarize RecentRows = count(), RecentUsers = dcount(UserPrincipalName) by AppId, AppDisplayName;\nlet baseline =\n    union SigninLogs, AADNonInteractiveUserSignInLogs\n    | where TimeGenerated between (ago(15d) .. ago(1d))\n    | where ResultType == \"50196\"\n    | summarize BaselineRows = count() by AppId;\nrecent\n| join kind=leftouter baseline on AppId\n| extend BaselineRows = coalesce(BaselineRows, 0)\n| where BaselineRows == 0 or RecentRows > 3 * (BaselineRows / 14.0)\n| project AppDisplayName, AppId, RecentRows, RecentUsers, BaselineRows\n| sort by RecentRows desc"
    }
  ],
  "kqlSectionText": "Microsoft doesn't document which sign-in table 50196 is logged in. Check where it lands in your workspace:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"50196\"\n| summarize Rows = count(), Apps = dcount(AppId) by SourceTable\n```\n\nWhich apps, users and clients loop:\n\n```kusto\nunion SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"50196\"\n| summarize Rows = count(),\n            Users = dcount(UserPrincipalName),\n            IPs = dcount(IPAddress),\n            Agents = make_set(UserAgent, 5),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated)\n          by AppDisplayName, AppId, ResourceDisplayName\n| sort by Rows desc\n```\n\nA new looping app or a sudden spike. This compares the last day with the 14 days before it:\n\n```kusto\nlet recent =\n    union SigninLogs, AADNonInteractiveUserSignInLogs\n    | where TimeGenerated > ago(1d)\n    | where ResultType == \"50196\"\n    | summarize RecentRows = count(), RecentUsers = dcount(UserPrincipalName) by AppId, AppDisplayName;\nlet baseline =\n    union SigninLogs, AADNonInteractiveUserSignInLogs\n    | where TimeGenerated between (ago(15d) .. ago(1d))\n    | where ResultType == \"50196\"\n    | summarize BaselineRows = count() by AppId;\nrecent\n| join kind=leftouter baseline on AppId\n| extend BaselineRows = coalesce(BaselineRows, 0)\n| where BaselineRows == 0 or RecentRows > 3 * (BaselineRows / 14.0)\n| project AppDisplayName, AppId, RecentRows, RecentUsers, BaselineRows\n| sort by RecentRows desc\n```\n\nThe factor of 3 is a starting point. Non-interactive rows are aggregated when application, user, IP address, status and resource ID match, so a single row can stand for many requests. Count rows to compare apps, not to count requests.",
  "fix": {
    "short": "Developer: set up token caching, call `AcquireTokenSilent` before any interactive call, don't retry in a tight loop, and honour `Retry-After` for app-only tokens. User: close the app or tab and reopen it, or sign out and back in. If it keeps happening, report it to the app owner.",
    "steps": "**For the user:** close the app or browser tab completely and open it again. If that doesn't help, sign out of the app, clear its cached sign-in, and sign in again. If it keeps happening, report it to whoever runs the app; the fix is in the app.\n\n**For developers:** Microsoft's throttling page lists the usual causes:\n1. *\"Token caching is not setup correctly.\"*\n2. Not calling the silent method (`AcquireTokenSilent`) before an interactive or username/password call.\n3. Asking for a scope that doesn't apply to personal Microsoft accounts, such as `User.ReadBasic.All`, *\"resulting in cache misses.\"*\n\nReuse tokens until they are close to expiry. Don't retry in a tight loop when a call fails. For app-only tokens, honour the `Retry-After` header. MSAL throws `MsalThrottledServiceException` or `MsalThrottledUiRequiredException` when it throttles on the client side; the page notes that *\"If MSAL would not apply client-side throttling the application would still not be able to acquire tokens.\"*\n\n**For admins:**\n1. Use the second query to find the app, then give its owner the time window and the user agents.\n2. If it is a script running under a user account, check that it gets one token and reuses it.\n3. If an unknown app or a single IP produces 50196 for many users, look at that IP's other sign-ins before you close it as a bug."
  },
  "labReproduced": null,
  "labSectionText": null,
  "labRuns": [],
  "verification": "documentation",
  "related": [
    "50058",
    "70043",
    "700082",
    "54005"
  ],
  "coveredBy": [],
  "sources": [
    {
      "title": "Microsoft Entra authentication and authorization error codes",
      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes"
    },
    {
      "title": "Client and server throttling (MSAL.NET)",
      "url": "https://learn.microsoft.com/en-us/entra/msal/dotnet/advanced/client-and-server-throttling"
    },
    {
      "title": "Non-interactive user sign-ins",
      "url": "https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-noninteractive-sign-ins"
    },
    {
      "title": "SigninLogs table reference (Azure Monitor)",
      "url": "https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/signinlogs"
    }
  ],
  "published": "2026-10-30",
  "lastUpdated": "2026-10-30",
  "lastVerified": null,
  "license": "CC BY 4.0",
  "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
  "licenseNote": "CC BY 4.0 covers the author's own text and classification fields. Quoted Microsoft error wording belongs to Microsoft, is quoted with its source and can change.",
  "attribution": "Bram Brinkmeier, \"AADSTS50196: App is stuck in a sign-in loop (fix)\", https://brambrinkmeier.com/entra/aadsts/50196/ (CC BY 4.0)",
  "microsoftErrorLookup": "https://login.microsoftonline.com/error?code=50196"
}
