Why your synchronized blocks silently throttle 100,000 virtual threads down to 8, and the mechanical fixes that actually restore throughput
The Migration That Made Things Worse
Your team spent two sprints migrating to virtual threads. The PR was clean — swap the executor, remove the thread pool sizing, delete the bulkhead configs. You deploy to staging, run your load test, and watch throughput drop by 40%. CPU utilization sits at 8%. Your carrier threads are pegged at 100%. The service is doing less work while appearing more busy. This is not a bug in your code in the traditional sense. This is pinning, and it just turned your Loom migration into a performance regression.
I hit this exact scenario on a service that handles DynamoDB calls through the AWS SDK’s HTTP connection pool. The pool internally uses synchronized blocks to manage connection checkout and return. Under low concurrency, everything looked fine. Under production load with a few hundred concurrent requests, virtual threads stacked up waiting for carrier threads that were pinned inside those synchronized regions. Effective concurrency collapsed from “unlimited virtual threads” to exactly the number of carrier threads, which on our ECS tasks meant 4. Four concurrent DynamoDB calls. On a service that previously ran 200 platform threads without issue. Our p99 latency went from 45ms to over 1.2 seconds.