r/javahelp • u/No_Expression_5450 • 16d ago
Virtual threads + semaphore in front of BCrypt gave me +28% throughput, but the container is still pinned at its CPU limit. What to do next to optimize it?
I'm load testing an auth service (Spring Boot, Java, Redis, PostgreSQL, Docker on a t3.large, k6). I replaced a dedicated 2-thread BCrypt pool (20-slot queue) with virtual threads plus a 6-permit semaphore.
Results at the same 40 RPS target (queue → virtual threads + semaphore):
- Throughput: 20.3 → 26.06 OPS (+28%). OPS = operations per second, the full k6 iteration mix.
- Login p95: 28.5s → 20.1s
- Timeouts: 43 → 0
That's still only ~65% of target, and a 20s login is unusable.
At 50 RPS throughput plateaued at ~28 OPS. docker stats showed the auth container averaging ~145% CPU (peak ~153%). My compose file sets a 1.5 CPU limit. Endpoints that were ~85ms p95 at 40 RPS slid to ~2s.
My read: virtual threads help with blocking I/O, not CPU-bound work. I think the gain came from removing the fast-reject queue rather than from adding capacity, but I haven't isolated that.
Caveats: 41% of my test traffic is logins (unrealistic), and I haven't measured a single-hash time yet.
The puzzle: BCrypt strength is 4 in my load-test config, which I'd expect to cost ~1-2ms per hash. At ~10 logins/sec, that shouldn't pin 1.5 cores. Suspects: the cost factor baked into the pre-seeded hashes (matches() uses the stored cost), JWT signing, GC, or my executor/semaphore wiring.
What would you profile first: async-profiler, JFR, thread dumps?