Skip to content

Intermittent HTTP 500 errors under concurrent request loads #3116

@chubes4

Description

@chubes4

Summary

Studio serves intermittent HTTP 500 errors under concurrent request loads that occur during normal site use — a browser page load firing parallel requests for HTML, CSS, JS, REST API, images, etc. Users see blank pages or broken assets until they refresh.

The symptom is visible at ordinary concurrency levels (10–25 parallel requests — less than a typical WordPress admin page load). A fresh install with Twenty Twenty-Five theme reproduces it.

Reproduction

With any Studio site running:

# Warm up
curl -s -o /dev/null http://localhost:8881/
sleep 2

# 15 concurrent requests × 5 rounds
for r in 1 2 3 4 5; do
  for i in $(seq 1 15); do
    curl -s -o /dev/null -w "%{http_code} " "http://localhost:8881/?r=${r}i=${i}" &
  done
  wait
  echo " <- round $r"
done

Observed on stock Studio (1.7.8)

500 500 500 200 200 200 200 200 200 200 200 200 200 200 200  <- round 1
500 500 500 500 500 500 200 200 200 200 200 200 200 200 200  <- round 2
500 500 500 500 500 200 200 200 200 200 200 200 200 200 200  <- round 3
...

First 3–7 requests of each concurrent burst fail. Body is literally Internal Server Error (21 bytes) from Playground's express error-catch path in start-server.ts.

Root Cause

The bug lives in @wp-playground/cli and @php-wasm/universal, not in Studio itself. Two overlapping issues:

1. Pool proxy releases workers before PHP finishes

handleRequest calls playgroundPool.requestStreamed() through the object pool proxy. The proxy releases the worker when requestStreamed() resolves — but that resolves when streaming starts, not when PHP finishes. The worker's SinglePHPInstanceManager is still acquired. A subsequent request routed to that "free" worker hits isAcquired === true and throws → 500.

2. Crashed workers never evicted from the pool

When a worker thread exits unexpectedly (e.g. EADDRINUSE during a port race), its API proxy stays in the pool. Requests routed to the dead proxy fail, release back, and the cycle continues. The pool permanently shrinks.

Upstream Fix

Both issues are fixed upstream (PR open, needs review/merge):

The fix switches handleRequest to buffered request() with a callback pattern so the full PHP lifecycle stays within the pool proxy's scope, and adds __removeInstance() to createObjectPoolProxy wired to the onExit handler so dead workers get evicted.

Verified fix

Built the Playground PR locally and installed it into Studio via the tarball self-hosting workflow, then re-ran the concurrency benchmark against the resulting dev build:

  • 15 concurrent × 5 rounds: 75/75 pass
  • 25 concurrent × 10 rounds: 250/250 pass
  • Zero 500s

Impact on Studio

Every Studio user hits this whenever their browser fires concurrent requests during a page load — which is most of them. The symptom is often dismissed as "weird, refresh worked" but it degrades:

  • Page load reliability (broken CSS/JS/images on first load)
  • REST API calls from the block editor (autosave, block loading)
  • External clients doing parallel requests (mobile apps, staging syncs, benchmarks)
  • WP-CLI commands that internally parallelize

Action Needed

Once WordPress/wordpress-playground#3494 merges and a new @wp-playground/* version is published, Studio bumps the pinned version in apps/cli/package.json (currently 3.1.19) and rebuilds.

Filing here so:

  1. Studio users encountering the 500s have a public explanation.
  2. There's a Studio-side tracker for the dep bump when the Playground PR merges.
  3. Studio team has visibility into a user-facing bug whose fix is waiting on upstream review.

Environment

  • Studio 1.7.8 on macOS
  • Default Twenty Twenty-Five theme, no additional plugins
  • Reproduced against every Studio site tested

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions