langgenius/dify · warning · EmailPasswordLoginLimitError
email_code_login_limit
email_code_login_limit
Error message
Too many incorrect password attempts. Please try again later.
What it means
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.
Source
Thrown at api/controllers/console/auth/login.py:134
@console_ns.expect(console_ns.models[LoginPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultOptionalDataResponse.__name__])
@decrypt_password_field
@model_validate(LoginPayload)
def post(self, req_data: LoginPayload):
"""Authenticate user and login."""
request_email = req_data.email
normalized_email = request_email.lower()
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(
normalized_email
):
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)
raise AccountInFreezeError()
is_login_error_rate_limit = AccountService.is_login_error_rate_limit(normalized_email)
if is_login_error_rate_limit:
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.LOGIN_RATE_LIMITED)
raise EmailPasswordLoginLimitError()
invite_token = req_data.invite_token
invitation_data: InvitationDetailDict | None = None
if invite_token:
invitation_data = RegisterService.get_invitation_with_case_fallback(
None, request_email, invite_token, session=db.session()
)
if invitation_data is None:
invite_token = None
try:
if invitation_data:
data = invitation_data.get("data", {})
invitee_email = data.get("email") if data else None
invitee_email_normalized = invitee_email.lower() if isinstance(invitee_email, str) else invitee_email
if invitee_email_normalized != normalized_email:
_log_console_login_failure(
email=normalized_email,View on GitHub (pinned to ef8544b173)
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.
Example fix
# before
is_login_error_rate_limit = AccountService.is_login_error_rate_limit(normalized_email)
if is_login_error_rate_limit:
raise EmailPasswordLoginLimitError()
# after - surface remaining lockout time to the client
if AccountService.is_login_error_rate_limit(normalized_email):
ttl = redis_client.ttl(f'login_error_rate_limit:{normalized_email}')
raise EmailPasswordLoginLimitError(description=f'Try again in {ttl} seconds') Defensive patterns
Strategy: retry
Validate before calling
# Client-side: enforce a local attempt counter before hitting the server limit
import time
attempts = get_local_attempts(email)
if attempts >= 5:
wait = LOGIN_LOCKOUT_DURATION # mirror server config
show_message(f'Too many attempts. Try again in {wait} seconds.')
return
submit_login(email, password) Type guard
null
Try / catch
from controllers.console.auth.error import EmailPasswordLoginLimitError
try:
do_login()
except EmailPasswordLoginLimitError:
schedule_retry(after_seconds=LOGIN_LOCKOUT_DURATION)
offer_password_reset() Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of langgenius/dify@ef8544b173 (2026-08-12).
Data as JSON: /api/errors/630225c494afb748.
Report an issue: GitHub.