Short answer
ERR_CACHE_MISS means Chrome may only show a page from its cache and has no copy. Usually you pressed Back to a page a form produced, so Chrome shows Confirm Form Resubmission instead of resending your data. Visitors: reload only if repeating the action is harmless. Site owners: answer the POST with a 303 redirect (Post/Redirect/Get).Where to start
- If you are a visitor and the page came from a form (checkout, login, search, contact)decide whether to resubmit, or open the page fresh
- If you run the site and users hit it after pressing Backredirect after every POST
- If your Android app shows net::ERR_CACHE_MISS in a WebViewcheck the INTERNET permission and the cache mode
- If it appears without pressing Back, or on pages with no form involvedrestored tabs and cache-only apps
What ERR_CACHE_MISS means
ERR_CACHE_MISS is a Chromium network error, number -400, which Chromium’s error list describes in one line: “The cache does not have the requested entry.” You see it in Chrome, Edge, Brave, Opera and Android’s WebView, which all share that network code. Firefox and Safari handle the same situation with their own messages and never show this code.
The reason it is tied to forms sits in how Chrome goes Back. For a history navigation to a page that was the answer to a POST, Chrome’s navigation code sets two load flags, LOAD_ONLY_FROM_CACHE and LOAD_SKIP_CACHE_VALIDATION: show the stored copy, and never contact the server. That is deliberate. Going Back must not place an order or send a payment a second time behind your back. If the HTTP cache holds the page, you see it instantly. If it does not, the load fails with error -400 and Chrome swaps in the resubmission page.
Chrome sets the same two flags when it restores a tab whose page answered a POST: after a restart that reopens your tabs, after a crash, or when you reopen a closed tab. So the same error can greet you without anyone pressing Back. That case comes from Chromium’s source; our test covered Back only.
The copy is usually missing because the server told the browser not to keep it. A response with Cache-Control: no-store must not be stored by any cache (RFC 9111, section 5.2.2.5), and every PHP page that calls session_start() sends that header by default. The back/forward cache, which keeps whole pages in memory, could have saved the day, but Google’s own bfcache guide says pages with no-store have historically not been eligible for it either.
Key facts
- Error code
- net::ERR_CACHE_MISS (-400)
- Shown by
- Chrome, Edge, Brave, Opera, Android WebView
- Page heading after a form
- Confirm Form Resubmission
- Page heading otherwise
- This site can’t be loaded from the cache
- Typical trigger
- Back, Forward or a restored tab, to a POST result
- Header that causes it
- Cache-Control: no-store
- Server-side fix
- 303 See Other after the POST
- Dangerous "fix"
- Confirming a reload without thinking: it sends the form again
Chrome picks the wording from the request method (Chromium’s localized_error.cc). After a POST the page reads, word for word:
Confirm Form Resubmission
This webpage requires data that you entered earlier in order to be properly
displayed. You can send this data again, but by doing so you will repeat any
action this page previously performed.
Press the reload button to resubmit the data needed to load the page.
ERR_CACHE_MISSThere is no Reload button on that page; Chromium removes it for POST errors, so the only way on is the browser’s own reload. For any other request the same error code gets a different page: “This site can’t be loaded from the cache” and “The saved (cached) copy of this site was unreadable.”, with a Reload button. That version is rare in a desktop browser and is covered further down.
Fixes for visitors
Nothing is broken on your computer. The page you went back to was the result of data you sent, and the site asked browsers not to keep it. Work through these in order:
- 1Decide whether sending the form again is safeReload sends the same data again, and the site processes it again. For a search or a filter that is harmless. For a checkout, a payment, a booking or a message, check your email or your account for a confirmation first. In our test, a scripted reload on the error page produced a second
POSTin the server log. - 2To see the page without resending, open it freshClick the site’s own link to the page, for example your order list or the search page, or type the address. That is a normal
GETrequest and never asks about form data. - 3To repeat a harmless action, reload and confirmPress F5 or Ctrl+R (Cmd+R on a Mac). Chrome first asks again in a dialog whether to resend the data. Only when you confirm does it send the form again and show the fresh result.
- 4If it shows up without pressing BackA tab Chrome restored after a restart or a crash, or a closed tab you reopened, behaves like Back when its page came from a form. Open the page fresh as in step 2. For pages with no form at all, see the section on it.
What does not help for the form case: clearing the browsing data (the page was never stored, so there is nothing to repair), flushing DNS, resetting Winsock, resetting Chrome’s settings or updating Chrome (current Chrome behaves this way on purpose). A hard reload with Ctrl+Shift+R is still a reload: Chrome asks the same question and, if you confirm, sends the form again. Those steps fill the top hosting guides for this error, but none of them changes a single header on the site.
Site owner: answer every POST with a redirect
The fix that removes the error for every visitor is the Post/Redirect/Get pattern. The form’s POST does the work, then answers with a redirect instead of a page. The browser follows it with a GET, and that GET page is the one in the history. Going Back or reloading now repeats a harmless GET, never the form.
Use 303 See Other. RFC 9110 defines it for exactly this case: the output of a POST action is shown at a different URL, and the client always follows it with GET. A 302 works in every current browser but leaves the method change to convention; a 307 or 308 keeps the POST and its body, which is the opposite of what you want here. The redirect status code comparison has all five side by side.
PHP: before and after
<?php
session_start(); // sends Cache-Control: no-store by default
$id = save_order($_POST); // your code
?>
<h1>Thank you, order <?= (int) $id ?> is saved.</h1><?php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = save_order($_POST); // do the work once
$_SESSION['flash'] = 'Order saved.';
header('Location: /orders/' . $id, true, 303);
exit; // send nothing else
}
// GET /orders/123 is rendered by its own script from the database- Call
exitafter the header. Without it the script keeps running and may print the old result page under the redirect. - Redirect on validation errors too. Store the errors and the entered values in the session, redirect back to the form with
303, and show them once. A form re-rendered straight from thePOSTis a POST result again. - The result page may keep
no-store. Chrome fetches aGETpage again when its copy is missing; onlyPOSTresults are restricted to the cache. In our test the/thanks.phppage sentno-store, and Back simply loaded it again from the server. - Frameworks have it built in. Express:
res.redirect(303, "/orders/" + id). Laravel’sredirect()and Django’sHttpResponseRedirectsend a302, which browsers also follow withGET.
Check the result from any shell. You want a 303 with a Location and no page:
curl -si -d item=test https://example.com/order.php \
| grep -iE '^(HTTP|location|cache-control)'HTTP/1.1 303 See Other
Cache-Control: no-store, no-cache, must-revalidate
Location: /thanks.php-d already makes curl send a POST, so no -X POST is needed. Over HTTP/2 the status line reads HTTP/2 303 and the header names are lowercase; the grep -i matches both.
nginx: look for a site-wide no-store
nginx cannot do Post/Redirect/Get for your application, but its configuration can be the source of the header. A Cache-Control "no-store" in the server block applies to every response from that site, POST results included. Find it:
sudo grep -rn 'no-store' /etc/nginx/Scope it to the pages that hold private data and send no-cache elsewhere. no-cache still makes the browser check with the server before reusing a copy on a normal visit, but it allows the copy to be stored, so Back to a form result works. A map keeps the rule in one place, which matters because PHP requests usually end in a separate location ~ \.php$ block after try_files, and headers set in location / never reach them:
map $request_uri $page_cache_control {
~^/account/ "no-store"; # private area: never stored
default "no-cache"; # stored, revalidated on normal visits
}add_header Cache-Control $page_cache_control always;add_headerdoes not replace what PHP sends. If PHP already sentCache-Control, the response then carries two of them. Remove the upstream one withfastcgi_hide_header Cache-Control;only where you mean to override it.- A
locationwith its ownadd_headerdrops the inherited ones by default. Long-lived static assets keep their own rule that way; repeat your security headers there or keep them in an included snippet. The 304 Not Modified guide shows the cache rules for static files. - Test before reloading:
sudo nginx -t && sudo systemctl reload nginx.
Which Cache-Control headers cause it
We sent the same form to the same PHP script with one header changed at a time, then clicked a link, pressed Back, and noted what Chrome did. Each case ran twice, with the back/forward cache on and with it switched off, so the HTTP cache alone decided. Both runs gave the same answer:
| Header on the POST result | Back in Chrome 153 | Server contacted on Back? |
|---|---|---|
Cache-Control: no-store | ERR_CACHE_MISS, Confirm Form Resubmission | No |
PHP default after session_start() (no-store, no-cache, must-revalidate) | ERR_CACHE_MISS | No |
Cache-Control: no-cache | Page shown from the cache | No |
Cache-Control: max-age=0 | Page shown from the cache | No |
Cache-Control: private, no-cache | Page shown from the cache | No |
| No Cache-Control header at all | Page shown from the cache | No |
303 redirect to a GET page with no-store | GET page loaded again | Yes, a GET |
The rule behind the table: only no-store stops the copy from existing, and Chrome skips validation for history navigations, so no-cache and max-age=0 do not matter on Back. That makes no-store on a page that answers a POST the one server-side setting that produces this error.
PHP sessions send no-store by default
PHP’s session.cache_limiter setting defaults to nocache, so every script that starts a session sends the headers below, whatever your own code does. This is what PHP 8.2.35 sent for each value:
session_cache_limiter() values, headers sent by PHP 8.2.35
- nocache (default)
- Cache-Control: no-store, no-cache, must-revalidateplus Expires: Thu, 19 Nov 1981 08:52:00 GMT and Pragma: no-cache
- private
- Cache-Control: private, max-age=10800plus the same 1981 Expires date and Last-Modified
- private_no_expire
- Cache-Control: private, max-age=10800plus Last-Modified, no Expires
- public
- Cache-Control: public, max-age=10800plus Expires three hours ahead; never for pages with personal data
10800 seconds is the default session.cache_expire of 180 minutes. Switching to private_no_expire also made Back work in our test, but it lets the browser keep pages with personal data for three hours. Post/Redirect/Get fixes the error without weakening that, which is why it is the better fix.
To see which Cache-Control header a public page sends, paste its address into our HTTP header checker.
net::ERR_CACHE_MISS in an Android WebView
In an Android app the error usually has nothing to do with forms. It appears on the very first loadUrl() because the WebView is not allowed to use the network and can only look in its cache. Android’s WebSettings reference spells out why: network loads are blocked by default when the app does not hold the INTERNET permission.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application ...>
...
</application>
</manifest>- Placement and spelling matter. The line belongs to
<manifest>, not inside<application>, and the name is exactlyandroid.permission.INTERNET. Reinstall the app after changing it. - Cache mode.
LOAD_CACHE_ONLY(value 3) means “Don’t use the network, load from the cache”, so anything not cached fails with this error. The default isLOAD_DEFAULT(-1). - Blocked network loads.
setBlockNetworkLoads(true)has the same effect as a missing permission. With the permission present the default isfalse. - Back to a POST result. The
setCacheMode()documentation says that when navigating back, content is retrieved from the cache without revalidation. A WebView that goes Back to a form result behaves like Chrome, and the fix is Post/Redirect/Get on the site.
webView.settings.cacheMode = WebSettings.LOAD_DEFAULT
webView.settings.blockNetworkLoads = false // throws SecurityException without INTERNETERR_CACHE_MISS without a form
For ordinary pages Chrome goes Back, or restores a tab, with only LOAD_SKIP_CACHE_VALIDATION: it uses a stored copy when there is one and asks the network when there is not. A plain page therefore cannot produce ERR_CACHE_MISS through normal browsing. The cache-only loads that Chromium’s source and Android’s documentation confirm are these:
- A restored tab that showed a form result. After a restart, a crash or reopening a closed tab, Chrome loads such a page from the cache only, exactly as on Back. You get the form variant of the error; open the page fresh through the site’s own link.
- A cache-only client. WebViews in
LOAD_CACHE_ONLYmode and apps without network access, as above. This is where the “can’t be loaded from the cache” variant usually comes from.
Forum answers also blame browser extensions and service workers. We found no path in Chromium’s source where either one causes this error and could not reproduce it, so treat them as unconfirmed. If it only happens with extensions on, test in an Incognito window, where extensions are off unless you allowed them, and turn them off one by one.
If the cause is on the server instead, for example the connection dropping, Chrome shows a different code. The ERR_CONNECTION_RESET guide covers dropped connections, and ERR_SSL_PROTOCOL_ERROR a broken HTTPS port.
Symptoms, causes and fixes
Symptom, cause, fix
Symptom: Back after a checkout or login shows Confirm Form Resubmission and
ERR_CACHE_MISS- Cause
- The page was the answer to a
POSTand was sent withCache-Control: no-store, often by PHP’s session defaults. - Fix
- Redirect after the
POSTwithheader('Location: /done', true, 303); exit;. Visitors open the page fresh instead of reloading.
Symptom: Duplicate orders, messages or sign-ups in the logs, a few seconds or minutes apart
- Cause
- Visitors reloaded a POST result and confirmed resubmission, or a tester had DevTools' Disable cache on.
- Fix
- Post/Redirect/Get, plus a one-time token in the form that the server accepts once. Untick Disable cache when testing Back.
Symptom:
net::ERR_CACHE_MISSon the first page an Android app loads- Cause
- No
android.permission.INTERNETin the manifest, or it sits inside<application>, so network loads are blocked. - Fix
- Add
<uses-permission android:name="android.permission.INTERNET" />directly under<manifest>and reinstall.
Symptom: WebView shows only pages it has seen before
- Cause
setCacheMode(LOAD_CACHE_ONLY)orsetBlockNetworkLoads(true).- Fix
- Set
LOAD_DEFAULTandblockNetworkLoads = false.
Symptom: Confirm Form Resubmission and
ERR_CACHE_MISSright after Chrome reopened its tabs, with no Back pressed- Cause
- Chrome restored a tab whose page answered a
POSTand loads it from the cache only; theno-storepage is not there. - Fix
- Open the page fresh through the site's own link. Site owners: Post/Redirect/Get, so no tab ever holds a POST result.
Symptom: This site can't be loaded from the cache, on a page with no form
- Cause
- A client that loads from the cache only, such as a WebView in
LOAD_CACHE_ONLYmode or without network access. Extensions are blamed often but unconfirmed. - Fix
- Check the app's cache mode and network permission. If it only happens with extensions on, test in Incognito and disable them one at a time.
How we reproduced it
A three-page PHP app on a local test server: a form, a result page and a link to a second page. A script drove headless Chrome through form, result, link, Back and Reload, and the server logged every request. The scripted reload goes through the DevTools protocol, which skips the dialog Chrome shows a person before it resends a POST; in a normal window that question comes first. The logs below are shortened to time, status and request. Before the change, the form posted to a result page that started a session:
[02:56:12] [200]: GET /form.php?to=result-nostore.php
[02:56:12] [200]: POST /result-nostore.php <- form sent
[02:56:12] [200]: GET /other.html <- link clicked
<- Back: no request at all
Chrome: page.goBack: net::ERR_CACHE_MISS (POST /result-nostore.php)
[02:56:13] [200]: POST /result-nostore.php <- scripted Reload, no dialog: form sent again[02:56:14] [200]: GET /form.php?to=prg.php
[02:56:14] [303]: POST /prg.php <- form sent, redirect
[02:56:14] [200]: GET /thanks.php
[02:56:14] [200]: GET /other.html <- link clicked
[02:56:14] [200]: GET /thanks.php <- Back: normal page
[02:56:15] [200]: GET /thanks.php <- Reload: GET, no second orderHow we tested
- OS image
- Debian 12
- Versions
- Chrome for Testing 153.0.8010.12 (headless)PHP 8.2.35 built-in servercurl 7.88.1Playwright 1.56.1
- Tested on
- Where
- Local test server on 127.0.0.1, one Linux machine
- Method
- Form POST, link, Back, Reload (scripted, which skips Chrome's resubmit dialog); seven Cache-Control variants; each run with and without the back/forward cache; DevTools Disable cache via the DevTools protocol; server log checked for every POST
Not tested by us: Firefox, Safari and a real Android device. The WebView section relies on Android’s WebSettings reference, the Chrome internals on Chromium’s source (net_error_list.h, navigation_request.cc, navigation_controller_impl.cc, localized_error.cc), both read on 2 November 2026. Restored tabs and the reload dialog come from that source, not from our test.
On a VMHeaven Standard KVM you have root on the web server, so the nginx scope, the PHP session settings and the redirect are all yours to change, and the curl check above shows within a minute whether the 303 arrives.
Frequently asked
What does ERR_CACHE_MISS mean?
It is Chromium network error -400, 'The cache does not have the requested entry.' Chrome shows it when it may only load a page from its cache and no copy is there. Almost always you went Back or Forward to a page that answered a form (a POST), or Chrome restored such a tab after a restart, a crash or reopening a closed tab. The site sent that page with Cache-Control: no-store, so Chrome shows Confirm Form Resubmission instead of sending your data again.
How do I fix ERR_CACHE_MISS in Chrome?
If the page came from a search or filter, press reload and confirm in the dialog Chrome shows: it then sends the form again. If it came from a checkout, payment or booking, check your email or account first, then open the page fresh through the site's own link instead of reloading. Clearing the cache, flushing DNS or resetting Chrome does not help, because the page was never stored.
How do I stop Confirm Form Resubmission on my website?
Use Post/Redirect/Get: handle the POST, then answer with a 303 See Other redirect to a normal page, for example header('Location: /orders/123', true, 303); exit; in PHP. Back and Reload then repeat a harmless GET. Removing no-store also stops the error, but it lets browsers keep pages that may hold personal data.
Is it safe to resubmit the form?
Only if repeating the action is harmless. Resubmitting sends the same data to the server again, which can place a second order, payment or message. In our test a reload on the error page produced a second POST in the server log.
Why does my Android WebView show net::ERR_CACHE_MISS?
Usually the app lacks the INTERNET permission, so the WebView blocks network loads and can only read its cache. Add <uses-permission android:name="android.permission.INTERNET" /> directly under <manifest> and reinstall. Also check that the cache mode is not LOAD_CACHE_ONLY and that setBlockNetworkLoads is false.