You type your username and password on wp-login.php, press Log In, and the page reloads with empty fields. No error. Your address bar may show something like wp-login.php?redirect_to=https%3A%2F%2Fexample.com%2Fwp-admin%2F&reauth=1. Or you see one of two red messages: Error: Cookies are blocked or not supported by your browser. You must enable cookies to use WordPress. or Error: Cookies are blocked due to unexpected output.
Your password is almost certainly fine. WordPress accepted it, tried to hand your browser a login cookie, and then on the very next request could not find or trust that cookie, so it sent you back to log in again. This guide shows you which cookie went missing and why. If your browser shows ERR_TOO_MANY_REDIRECTS instead of the login form, that is a redirect loop between URLs and our separate guide on that error covers it.
What is happening
Logging in to WordPress is a short conversation between your browser and the server:
- When you load
wp-login.php, WordPress sets a test cookie called wordpress_test_cookie.
- When you submit the form, WordPress checks that the test cookie came back. If it did not, you get “Cookies are blocked or not supported”.
- If your password is right, WordPress responds with a 302 redirect and several
Set-Cookie headers.
- Your browser follows the redirect to
/wp-admin/ and sends the cookies back. WordPress checks them, and if anything is wrong it sends you to wp-login.php?reauth=1.
These are the cookies involved. HASH is an MD5 hash of your siteurl, so the cookie names change if siteurl changes.
| Cookie |
Purpose |
Path it is set for |
wordpress_HASH |
Auth cookie for wp-admin over plain HTTP |
/wp-admin and /wp-content/plugins |
wordpress_sec_HASH |
Auth cookie for wp-admin over HTTPS, marked Secure |
/wp-admin and /wp-content/plugins |
wordpress_logged_in_HASH |
Tells the front end you are logged in (admin bar, caching rules) |
The path of your home URL, usually / |
wordpress_test_cookie |
Checks that your browser accepts cookies at all |
Home and site paths |
Each auth cookie holds your username, an expiry time, a session token and a signature built from the keys and salts in wp-config.php. WordPress rejects the cookie if the signature does not match, if it has expired by the server’s clock, or if the session token is no longer stored in your user record (the session_tokens user meta). Every login loop comes down to a cookie that was never set, was scoped to the wrong host or path, was dropped by a cache, was the wrong kind, or failed validation.
Before you start
You need SSH, SFTP or your hosting file manager, ideally with WP-CLI. Back up wp-config.php and run wp db export before changing anything.
How to run the commands in this guide. Server commands run in an SSH terminal from your site folder, the one that contains wp-config.php. WP-CLI is called wp on most hosts and wp-cli on some, so swap the name if needed. curl commands run on your own computer: built in on macOS and Linux, and available as curl.exe in Windows PowerShell or inside WSL. Browser steps use DevTools, which you open with F12, or Cmd+Option+I on a Mac.
Step 1: Rule out the browser, then watch the cookies
Try a private window in a different browser. If that works, your usual browser holds a stale cookie or an extension is interfering: clear cookies for just your domain and test with extensions off.
If it fails everywhere, watch the login happen:
- Open DevTools, go to the Network tab and check Preserve log, so entries survive the redirect.
- Log in. Click the
POST request to wp-login.php.
- In its Response Headers, look for
Set-Cookie lines for wordpress_sec_ or wordpress_ and wordpress_logged_in_.
- Click the next request, to
/wp-admin/, and look at its Request Headers for a Cookie line.
How to read what you see:
- No Set-Cookie on the POST response: something stripped it (a cache, CDN or proxy), or PHP printed output before the headers. Steps 4 and 5.
- Set-Cookie is present, with a yellow warning icon: Chrome blocked it. Hover the icon. A message about the Domain attribute being invalid for the current host means a COOKIE_DOMAIN problem (Step 2). A message about the Secure attribute means the cookie was marked Secure on a plain HTTP page.
- The redirect goes to a different host, for example from
www.example.com to example.com: the cookie belongs to one host and you are being sent to the other. Step 2.
- The cookie is sent to /wp-admin/ but you still bounce: the cookie failed validation, or WordPress expected the other cookie type. Steps 3, 6 and 7.
Step 2: Make the address, siteurl, home and cookie settings agree
Check what WordPress thinks its address is:
wp option get siteurl
wp option get home
grep -nE "WP_HOME|WP_SITEURL|COOKIE_DOMAIN|COOKIEPATH|SITECOOKIEPATH|ADMIN_COOKIE_PATH|FORCE_SSL_ADMIN" wp-config.php
Run these over SSH in the site folder. The first two print the stored WordPress Address and Site Address. The grep searches wp-config.php for constants that override addresses and cookie scope; -n shows line numbers and -E lets the pipe characters mean “or”. Good output: both options show the exact address you type (same scheme, same www or not), and the grep shows nothing or matching values. Bad output: http where you use https, a missing or extra www, or a COOKIE_DOMAIN naming an old domain or a staging domain.
Fix mismatched options:
wp option update siteurl 'https://example.com'
wp option update home 'https://example.com'
Replace https://example.com with your canonical address, no trailing slash. If WP_HOME or WP_SITEURL exist in wp-config.php, they override these options, so edit those lines instead.
For cookie constants: on a normal single site, delete any COOKIE_DOMAIN, COOKIEPATH, SITECOOKIEPATH or ADMIN_COOKIE_PATH lines you did not add on purpose. WordPress works these out correctly from home and siteurl. A COOKIE_DOMAIN that does not match the host in the address bar makes browsers reject the cookie outright. Staging clones are the classic case: the live domain stays hard-coded and staging can never log in.
Step 3: Fix HTTPS detection behind a proxy, CDN or load balancer
WordPress decides whether a request is HTTPS with is_ssl(), which only checks two things: whether PHP’s $_SERVER['HTTPS'] is “on” or “1”, or whether the server port is 443. When TLS ends at a proxy, CDN, load balancer or container router and the proxy talks to PHP over plain HTTP, both checks fail. WordPress believes the visitor is on HTTP.
The result: WordPress can set one kind of auth cookie (plain wordpress_HASH) and then look for the other (wordpress_sec_HASH) on wp-admin, especially when FORCE_SSL_ADMIN is on. You never pass the check.
Confirm it
Create a temporary file called proto-check.php in your site folder with this content, load it in your browser over HTTPS, then delete it straight away:
<?php
header( 'Content-Type: text/plain' );
echo 'HTTPS: ', $_SERVER['HTTPS'] ?? '(not set)', "\n";
echo 'SERVER_PORT: ', $_SERVER['SERVER_PORT'] ?? '(not set)', "\n";
echo 'X-Forwarded-Proto: ', $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '(not set)', "\n";
This is a file you create on the server and visit at https://example.com/proto-check.php. It has no placeholders. Good output: HTTPS: on or SERVER_PORT: 443. Bad output: HTTPS: (not set), port 80, and X-Forwarded-Proto: https, which means a proxy terminated TLS and told PHP in a header that WordPress does not read on its own. Delete the file when you are done, because it reveals server details.
Fix it
Add this to wp-config.php above the “That’s all, stop editing” line:
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && false !== strpos( $_SERVER['HTTP_X_FORWARDED_PROTO'], 'https' ) ) {
$_SERVER['HTTPS'] = 'on';
}
This is a file you edit on the server. It makes WordPress trust the proxy’s X-Forwarded-Proto header, so only use it when every request passes through a proxy that sets or overwrites that header; otherwise anyone could send it. If your host offers a setting that passes HTTPS through to PHP, use that instead. Good outcome: the check file shows HTTPS: on and login sticks.
With Cloudflare, use SSL/TLS mode Full (strict) with a valid certificate on the server, not Flexible. Flexible sends every request to your server over HTTP, which is the root of many HTTPS detection problems.
Step 4: Stop caches from touching login and admin pages
A cache that stores wp-login.php or a redirect breaks logins: the test cookie never arrives, or your login cookies are dropped on the way back.
curl -sS -I https://example.com/wp-login.php
curl -sS -I https://example.com/wp-admin/
Run these on your own computer, replacing example.com with your domain. -I asks for headers only. Good output for wp-login.php: a 200 with Set-Cookie: wordpress_test_cookie=WP%20Cookie%20check, Cache-Control: no-cache, must-revalidate, max-age=0 (or similar), and no cache hit. Good output for /wp-admin/: a 302 to wp-login.php. Bad output: cf-cache-status: HIT, x-cache: HIT, x-litespeed-cache: hit, an Age header with a growing number, or a missing test cookie.
Fix it in every layer that caches:
- Caching plugin: exclude wp-login.php and wp-admin, and keep “cache logged-in users” off.
- Cloudflare: a Cache Rule with “Eligible for cache” (or an old “Cache Everything” Page Rule) matching the whole site will catch wp-login.php. Add a rule above it that bypasses cache when the URI path contains
/wp-admin or /wp-login.php, or when the Cookie header contains wordpress_logged_in.
- Nginx FastCGI cache or Varnish: skip the cache for those paths and for the logged-in cookie, as in the example below.
set $skip_cache 0;
if ($request_uri ~* "/wp-admin/|/wp-login\.php") { set $skip_cache 1; }
if ($http_cookie ~* "wordpress_logged_in|wordpress_sec_|comment_author|wp-postpass") { set $skip_cache 1; }
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
This goes in the Nginx server block for the site, on a server you manage. fastcgi_cache_bypass fetches a fresh response instead of a cached one, and fastcgi_no_cache stops that response from being saved. Test with nginx -t and reload Nginx after editing. Good outcome: the curl checks above show no cache hits on login or admin URLs.
Step 5: Fix “Cookies are blocked” messages
“Cookies are blocked due to unexpected output”
WordPress shows this when PHP had already sent output before it could send the cookie headers. The usual cause is whitespace or a byte order mark before the opening <?php in wp-config.php or a theme’s functions.php, or blank lines after a closing ?>. The PHP log often has a matching warning: “Cannot modify header information, headers already sent by (output started at FILE:LINE)”. That FILE and LINE is your culprit.
head -c 8 wp-config.php | od -c
tail -c 8 wp-config.php | od -c
Run these over SSH in the site folder, and repeat them for any file the PHP warning names. head -c 8 takes the first 8 bytes, tail -c 8 the last 8, and od -c shows each byte as a character. Good output starts with < ? p h p. Bad output shows 357 273 277 before it (a UTF-8 byte order mark) or spaces or \n before the <. At the end of the file, anything after a closing ?> is a problem; the simplest fix is to delete the closing tag entirely. Re-save the file as “UTF-8 without BOM” in your editor.
“Cookies are blocked or not supported by your browser”
The test cookie did not come back with the form. If other browsers work, check your browser’s cookie settings. If none work, a cache served wp-login.php without its Set-Cookie header (Step 4), COOKIE_DOMAIN or COOKIEPATH is wrong (Step 2), or the form was loaded from one host and posted to another.
Step 6: Check security plugins and session tokens
Security and login plugins hook into exactly this process. A custom login URL plugin, a two-factor step, a “one session per user” rule, a geo-block or an IP-based session lock can all send you back to the login form without an error. Confirm by switching them off from the server:
wp plugin list --status=active --fields=name,version
wp plugin deactivate PLUGIN_SLUG
Run these over SSH in the site folder. The first lists active plugins so you can spot security, login, firewall and redirect plugins. Replace PLUGIN_SLUG with a name from the first column. Good output: “Plugin ‘PLUGIN_SLUG’ deactivated.” Try logging in after each one.
Clear a user’s sessions
If the session_tokens record is corrupted, or a plugin limits sessions and counts stale ones, clear them:
wp user session list USERNAME --fields=login_time,expiration,ip
wp user session destroy USERNAME --all
Run these over SSH. USERNAME is the login name, email address or numeric user ID of the account that is looping; wp user list --fields=ID,user_login,user_email shows them. The first command lists current sessions with their times and IP addresses. The second deletes every session for that user, which logs them out everywhere. Good output: “Success: Destroyed all sessions.”
Reset every login cookie at once
If cookies were issued with old salts, for example after a migration that changed wp-config.php, rotating the salts invalidates all of them so everyone starts clean:
wp config shuffle-salts
Run this over SSH. It writes new random keys and salts into wp-config.php and logs everyone out. Good output is a “Success” line; an error about writing the file means a file ownership or permission problem.
Step 7: Check the server clock
WordPress puts an expiry time inside the auth cookie, based on the server’s clock: 48 hours by default, or 14 days if you check “Remember Me”. With Remember Me, the cookie’s Expires attribute is set the same way, and your browser judges it by its own clock. If the server or your computer is wrong by days, the browser may discard the cookie the moment it arrives, or WordPress may decide it has already expired.
date -u
Run this over SSH and on your own computer at the same moment, and compare both with your phone. Good: all within a minute or two. Bad: hours or days apart. A wrong server clock needs a ticket to your host; a wrong computer clock is fixed in your system settings.
Step 8: Multisite and mapped domains
On a subdomain multisite, WordPress sets COOKIE_DOMAIN to the network’s main domain with a leading dot, so one cookie covers site1.example.com and site2.example.com. A site mapped to a completely different domain, such as clientsite.com, cannot use a cookie for .example.com, so logging in on the mapped domain loops.
grep -nE "COOKIE_DOMAIN|DOMAIN_CURRENT_SITE|SUNRISE" wp-config.php
wp site list --fields=blog_id,url
wp --url=SITE_URL option get home
Run these over SSH. The grep shows the cookie domain, the main network domain and whether an old sunrise.php mapping drop-in is enabled. The second lists every site’s stored address. In the third, replace SITE_URL with the mapped site’s URL from that list; --url picks which network site WP-CLI acts on. Good output: stored addresses and home match what people type.
If COOKIE_DOMAIN is hard-coded to the main domain, remove it, or replace it with define( 'COOKIE_DOMAIN', $_SERVER['HTTP_HOST'] );, which scopes the cookie to whichever host the request arrived on. Leftover SUNRISE settings from a retired domain mapping plugin can also conflict with the mapping built into WordPress core.
Advanced: find out exactly why WordPress rejects the cookie
When cookies are sent but you still bounce, WordPress fires a specific action for each validation failure. A tiny must-use plugin can log which one, so you stop guessing. Create wp-content/mu-plugins/auth-cookie-debug.php:
<?php
foreach ( array( 'auth_cookie_malformed', 'auth_cookie_expired', 'auth_cookie_bad_username', 'auth_cookie_bad_hash', 'auth_cookie_bad_session_token' ) as $hook ) {
add_action( $hook, function () use ( $hook ) {
error_log( 'Auth cookie rejected: ' . $hook . ' on ' . ( $_SERVER['REQUEST_URI'] ?? '' ) );
} );
}
This is a file you create on the server. Files in mu-plugins load automatically, with no activation. Log in once, then read the PHP error log or wp-content/debug.log. How to read the result:
| Logged hook |
Meaning |
Where to look |
auth_cookie_malformed |
The cookie is missing or not in the expected format |
Wrong cookie type (secure versus plain), Step 3; or a proxy rewriting cookies |
auth_cookie_expired |
The expiry inside the cookie has passed by the server’s clock |
Server clock, Step 7 |
auth_cookie_bad_username |
The username in the cookie does not exist |
The user table, or a database prefix mismatch after a migration |
auth_cookie_bad_hash |
The signature does not match |
Keys and salts changed, or differ between servers behind a load balancer |
auth_cookie_bad_session_token |
The session token is not in session_tokens |
A security plugin ending sessions, or object cache issues; Step 6 |
No log line at all means the cookie never reached PHP: go back to Steps 1, 2 and 4. Delete the debug file when you are finished.
When to contact your host
Open a ticket if HTTPS detection fails at the proxy, the server clock is wrong, or a server cache keeps caching login pages. Send:
- The URL you log in at and the URL you are sent back to, including the full
redirect_to query string.
- The time of a failed attempt in UTC.
- The response headers of the login POST and the following
/wp-admin/ request, copied from DevTools (remove cookie values before you send them, since they are live credentials).
- Any Cloudflare Ray ID shown in the
cf-ray response header.
- What changed recently, such as a migration or a new CDN.
For a guided checklist, start at the Emergency Room, and use our diagnostic tools to check redirects and headers from outside your network. If you cannot get in at all because of a lost password, a missing admin role or a security lockout, see our guide on being locked out of WordPress admin.