{"record":{"id":"630225c494afb748","repo":"langgenius/dify","slug":"email-code-login-limit","errorCode":"email_code_login_limit","errorMessage":"Too many incorrect password attempts. Please try again later.","messagePattern":"Too many incorrect password attempts\\. Please try again later\\.","errorType":"error_code","errorClass":"EmailPasswordLoginLimitError","httpStatus":429,"severity":"warning","filePath":"api/controllers/console/auth/login.py","lineNumber":134,"sourceCode":"    @console_ns.expect(console_ns.models[LoginPayload.__name__])\n    @console_ns.response(200, \"Success\", console_ns.models[SimpleResultOptionalDataResponse.__name__])\n    @decrypt_password_field\n    @model_validate(LoginPayload)\n    def post(self, req_data: LoginPayload):\n        \"\"\"Authenticate user and login.\"\"\"\n        request_email = req_data.email\n        normalized_email = request_email.lower()\n\n        if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(\n            normalized_email\n        ):\n            _log_console_login_failure(email=normalized_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)\n            raise AccountInFreezeError()\n\n        is_login_error_rate_limit = AccountService.is_login_error_rate_limit(normalized_email)\n        if is_login_error_rate_limit:\n            _log_console_login_failure(email=normalized_email, reason=LoginFailureReason.LOGIN_RATE_LIMITED)\n            raise EmailPasswordLoginLimitError()\n\n        invite_token = req_data.invite_token\n        invitation_data: InvitationDetailDict | None = None\n        if invite_token:\n            invitation_data = RegisterService.get_invitation_with_case_fallback(\n                None, request_email, invite_token, session=db.session()\n            )\n            if invitation_data is None:\n                invite_token = None\n\n        try:\n            if invitation_data:\n                data = invitation_data.get(\"data\", {})\n                invitee_email = data.get(\"email\") if data else None\n                invitee_email_normalized = invitee_email.lower() if isinstance(invitee_email, str) else invitee_email\n                if invitee_email_normalized != normalized_email:\n                    _log_console_login_failure(\n                        email=normalized_email,","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/api/controllers/console/auth/login.py#L116-L152","documentation":"Raised by POST /console/api/login (HTTP 429, code email_code_login_limit) when AccountService.is_login_error_rate_limit(email) returns true. The counter login_error_rate_limit:<email> in Redis is incremented on each AccountPasswordError; once it exceeds LOGIN_MAX_ERROR_LIMITS (5), login is blocked for LOGIN_LOCKOUT_DURATION seconds.","triggerScenarios":"POST /console/api/login after more than 5 consecutive failed password attempts for the same normalized email within the LOGIN_LOCKOUT_DURATION window. The Redis key login_error_rate_limit:<email> holds a count > 5.","commonSituations":"User forgot their password and repeatedly tried wrong credentials; credential-stuffing or brute-force attempts triggering the lockout; shared email used by an automated client with stale credentials; Redis TTL misconfigured (LOGIN_LOCKOUT_DURATION too long) keeping the lock active.","solutions":["Wait for LOGIN_LOCKOUT_DURATION seconds (default defined in configs/feature) for the Redis key to expire, then retry.","Use the forgot-password flow to reset the password instead of continuing to guess.","An operator can clear the lock immediately by deleting the Redis key: DEL login_error_rate_limit:<email>.","Verify the user is submitting the correct (decrypted) password and that the @decrypt_password_field decorator is not double-encrypting on the client."],"exampleFix":"# before\nis_login_error_rate_limit = AccountService.is_login_error_rate_limit(normalized_email)\nif is_login_error_rate_limit:\n    raise EmailPasswordLoginLimitError()\n# after - surface remaining lockout time to the client\nif AccountService.is_login_error_rate_limit(normalized_email):\n    ttl = redis_client.ttl(f'login_error_rate_limit:{normalized_email}')\n    raise EmailPasswordLoginLimitError(description=f'Try again in {ttl} seconds')","handlingStrategy":"retry","validationCode":"# Client-side: enforce a local attempt counter before hitting the server limit\nimport time\n attempts = get_local_attempts(email)\nif attempts >= 5:\n    wait = LOGIN_LOCKOUT_DURATION  # mirror server config\n    show_message(f'Too many attempts. Try again in {wait} seconds.')\n    return\nsubmit_login(email, password)","typeGuard":"null","tryCatchPattern":"from controllers.console.auth.error import EmailPasswordLoginLimitError\ntry:\n    do_login()\nexcept EmailPasswordLoginLimitError:\n    schedule_retry(after_seconds=LOGIN_LOCKOUT_DURATION)\n    offer_password_reset()","preventionTips":["Cap client-side retries well below 5 to leave headroom.","Route users to the forgot-password flow after 2-3 failures instead of continuing.","Monitor LOGIN_LOCKOUT_DURATION and LOGIN_MAX_ERROR_LIMITS in config across environments."],"tags":["auth","login","rate-limit","brute-force","redis"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}