Allow longer time-to-first-byte for AI API requests (cURL low-speed timeout) - #3120
Merged
Merged
Conversation
…imeout) Fixes #3118. The mu-plugin injected into every Studio site by getStandardMuPlugins sets CURLOPT_LOW_SPEED_LIMIT=1024 + CURLOPT_LOW_SPEED_TIME=30, aborting any outbound HTTP request where transfer rate stays below 1 KB/s for 30 consecutive seconds. These values were correct for the original motivation (plugin/theme downloads, #1407/#1913) -- a dead mirror delivering zero bytes is a broken connection, and aborting fast is the right behavior. They are incorrect for AI API requests, now a common Studio use case for plugin developers building on top of OpenAI, Anthropic, Google, etc. Non-streaming wp_remote_post calls to LLM providers produce zero bytes on the socket until the model emits its first token. On flagship models with long prompts, that first token commonly takes 30 to 90 seconds to arrive. The watchdog fires before content is delivered and aborts with cURL error 28 ("Operation too slow"). Raise CURLOPT_LOW_SPEED_TIME from 30 seconds to 120 seconds. All other thresholds unchanged, including the 300-second http_request_timeout outer cap. A working plugin download produces far more than 1 KB/s throughout its lifetime, so the low-speed watchdog never triggers for that use case -- the #1913 Jetpack-on-3G test passes identically. Dead-connection detection still works; it takes 120 seconds to detect instead of 30, which is a small DX regression for an uncommon failure mode, and the 300-second outer cap still catches fully hung requests. The only observable behavior change: AI API requests that would have died at 30 seconds now complete normally (up to the 300-second outer cap, which is already enough for all current flagship models). Reproduced cURL 28 aborts from Data Machine's daily memory compaction against api.openai.com with gpt-5.4 on a ~30 KB prompt. Built the CLI locally, verified the compiled mu-plugin contains the new value via PHP reflection on the runtime closure (CURLOPT_LOW_SPEED_TIME = 120), re-ran the compaction task against the same flagship model on the same site, and confirmed end-to-end completion. AI assistance: I used Claude Code to research the history of this mu-plugin (tracing through #1407 and #1913), evaluate the side effects of each possible adjustment (raising the time vs. lowering the limit vs. an escape-hatch filter), and draft this commit message and the accompanying PR body. The engineering decision to raise the time threshold to 120 seconds (rather than add a filter or discriminate by URL) was mine after reviewing the options. I verified the fix end-to-end against a live Studio site and the original reproduction.
Collaborator
📊 Performance Test ResultsComparing 8263bf8 vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
gcsecsey
approved these changes
Apr 17, 2026
gcsecsey
left a comment
Member
There was a problem hiding this comment.
The change makes sense to me, thanks for the detailing the motivation for it in the PR description @chubes4!
I tested that the Jetpack installation still completes eventually on a slower connection, and found no regressions:
| connection speed | plugin install |
|---|---|
![]() |
![]() |
Contributor
Author
|
Thank you @gcsecsey! |
fredrikekelund
approved these changes
Jul 7, 2026
fredrikekelund
left a comment
Contributor
There was a problem hiding this comment.
This looks fine to me 👍 I did some light smoke testing and didn't spot any issues, so I'll go ahead and merge this
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Related issues
Fixes #3118.
Proposed Changes
CURLOPT_LOW_SPEED_TIMEfrom 30 seconds to 120 seconds ingetStandardMuPlugins'0-http-request-timeout.php.CURLOPT_CONNECTTIMEOUTstays at 30s,CURLOPT_LOW_SPEED_LIMITstays at 1024 B/s,http_request_timeoutstays at 300s.Background
The
0-http-request-timeout.phpmu-plugin was introduced in #1407 (May 2025, hard 60s timeout) and refined in #1913 (October 2025, replaced hard timeout with progress-based watchdog). Both PRs were correct for their motivation: plugin/theme downloads on slow connections. The #1913 design — abort if transfer stays below 1 KB/s for 30 consecutive seconds — fails fast on dead mirrors while letting working slow downloads (Jetpack 33 MB on 3G) complete.Since October 2025, AI-powered WordPress plugins (Data Machine, Intelligence, and others) have become a common Studio use case. Non-streaming
wp_remote_postcalls to LLM providers produce zero bytes on the socket until the model emits its first token. On flagship models with long prompts, that first token commonly takes 30 to 90 seconds. The watchdog fires before any content is delivered, aborting with cURL error 28.See #3118 for the full reproduction and context.
Side effects of the change
http_request_timeoutouter capThe only observable behavior change: AI API requests that were dying now complete. The only regression is that a truly hung connection takes 90 seconds longer to detect. The 300-second outer cap still catches fully broken requests. This is a small DX trade-off for an uncommon failure mode, in exchange for first-class AI API support.
No new escape-hatch filter, no URL-based discrimination, no new configuration knob. The one default value moves to a number that makes both use cases work.
Testing Instructions
Existing #1913 regression test (plugin download on throttled connection)
Per Bernardo's original methodology:
/wp-admin/plugin-install.phpShould pass identically before and after this change — the slow-but-working download produces far more than 1 KB/s throughout, so the low-speed watchdog never triggers regardless of whether the threshold is 30s or 120s.
New test case (slow-TTFB AI API request)
wp_remote_postto callhttps://api.openai.com/v1/chat/completions(e.g., Data Machine or any ai-http-client consumer).gpt-5.4or similar).Dead-connection regression test
wp eval 'var_dump(wp_remote_get(\"https://example.invalid/never-responds\"));'For a case that reaches the low-speed watchdog specifically, you'd need a server that accepts the connection but then sends zero bytes indefinitely (uncommon). That case would now take 120s to detect instead of 30s.
Verifying the change is live in WASM
Expected output after restart should include
CURLOPT_LOW_SPEED_TIME, 120.What this isn't
AI assistance disclosure
Per WordPress's AI Guidelines, disclosing meaningful AI assistance on this contribution:
All substantive design decisions and the testing were my own; AI was used for investigation, pattern recognition, and drafting.
Pre-merge Checklist