Disclosure: this analysis was done with the help of Claude (Anthropic) reading through the Dashboard 6.1.2 and extension source. I did the reporting and judgment calls myself, but wanted to be upfront about the method — treat the code references below as a starting point for your own review, not as verified-by-testing findings.
Symptom
Removing a site from Manage Sites shows “Removing the site. Please wait…” and sometimes never resolves — no success message, no error, no redirect. Checking back later (minutes to hours), the site turns out to have been removed anyway. This happens on most sites, not consistently reproducible, and never leaves anything in the PHP/webserver error logs.
Investigation
remove_site() → remove_website() in pages/page-mainwp-manage-sites-handler.php calls MainWP_Connect::fetch_url_authed( $website, 'deactivate' ) before removing the DB record, wrapped in try/catch.
In class/class-mainwp-connect.php, that single-site request sets:
// line 1603
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
...
// lines 1683-1684
$timeout = 20 * 60 * 60;
curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
MainWP_System_Utility::set_time_limit( $timeout );
Connect phase is capped at 10s, but total transfer time is capped at 20 hours. If a child site accepts the connection but is slow to respond (busy host, security plugin holding the request, etc.), this single “deactivate” call has no realistic upper bound. The same 20 * 60 * 60 constant is used in fetch_urls_authed() (line 782, used for chunked multi-site sync) — that seems like the appropriate place for a long timeout; reusing it for a single interactive action during site removal looks unintentional.
Because the call is in a try/catch, an eventual timeout or error doesn’t block removal — the DB record still gets deleted afterward. This matches the observed behavior: the site is gone even though the browser never showed a result.
Separately, in assets/js/mainwp.js, mainwp_get_remove_calback() calls jQuery.post(ajaxurl, data, function(response) {...}, 'json') with no .fail()/error handler. If the request above does eventually error at the HTTP level (5xx, empty/non-JSON body from a webserver-level timeout), the success callback never fires and the “Removing the site…” message is stuck permanently with no feedback.
Ruled out
Audited all extensions hooking mainwp_delete_site / mainwp_site_deleted that we have active: SSL Monitor, Staging, Sucuri, UpdraftPlus, Wordfence. All five only do a single synchronous DB DELETE in their hook — no network or file I/O. Not the cause, at least for these five.
Suggested direction
-
Give the single-site
deactivatecall (and similar interactive single-site actions) its own short timeout, independent of the bulk-sync timeout — happy to hear what you consider reasonable. -
Add a
.fail()handler inmainwp.jsso the UI always resolves to success or a clear error instead of hanging.
We’re not proposing a specific patch here — you know this code and its edge cases (staging sites, demo sites, retry logic) far better than we do. Just flagging what the code review turned up. Happy to provide more detail or test a fix if useful.