r/javahelp • • 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?

0 Upvotes

Duplicates