Skip to content
Back to skills

Virtual Thread Migration

ASecurity

Migrating an existing service to virtual threads as a staged programme rather than a flag: inventorying what each thread pool was implicitly limiting, auditing for pinning, file I/O and ThreadLocal caches, declaring the replacement limits before the flip, canarying one workload at a time, re-sizing the connection pool, and the rollback criteria. Use when a team plans to enable virtual threads service-wide, when a single flag is about to be flipped in production, when a migration made latency ...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 19, 2026
developmentgojavareactapidatabaseperformance

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add robsonkades/agent-skills --skill virtual-thread-migration --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Virtual Thread Migration?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Virtual Thread Migration
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/robsonkades-virtual-thread-migration/badge)](https://www.skillsdirectory.com/skills/robsonkades-virtual-thread-migration)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: virtual-thread-migration
description: >
  Migrating an existing service to virtual threads as a staged programme rather than a flag:
  inventorying what each thread pool was implicitly limiting, auditing for pinning, file I/O
  and ThreadLocal caches, declaring the replacement limits before the flip, canarying one
  workload at a time, re-sizing the connection pool, and the rollback criteria. Use when a
  team plans to enable virtual threads service-wide, when a single flag is about to be
  flipped in production, when a migration made latency worse, when the database or a
  downstream started failing after adoption, when newSingleThreadExecutor is about to be
  replaced and it was providing ordering, when log correlation or metrics broke after the
  change, or when a migration is proposed for a CPU-bound service. Not the sizing arithmetic
  (thread-sizing-and-virtual-threads), continuation and pinning internals
  (virtual-threads-internals), or choosing between reactive and thread-per-request, and the
  framework flags for it (reactive-and-virtual-thread-selection).
---

# Virtual Thread Migration

## Purpose

Move a working service onto virtual threads without discovering, in production, that the
thread pool being removed was the only thing bounding a downstream dependency.

The migration itself is easy — that is the trap. The hard parts are the properties that were
never written down: a pool size that was an admission limit, a single-threaded executor that
was mutual exclusion, a `ThreadLocal` that was a cache, a thread name that was a log filter.
Those properties can change silently in a change that appears to be about performance;
inspect which execution paths and contracts actually change.

## Workflow

Inspect the compiler/toolchain, deployed JDK/vendor/build, framework/client versions and effective
executor configuration. Standard virtual threads require Java 21+; Java 17 cannot use the virtual
factory examples. Java 24 removes monitor-only pinning under default HotSpot locking, and
ScopedValue is final only in Java 25. Check effective locking configuration as well as JDK version.
Do not upgrade Java/frameworks or enable preview merely to perform this migration.

Apply the stages needed for the requested decision and affected execution paths. A narrow API,
source or incident question can close from sufficient evidence without a service-wide inventory,
new benchmark or migration. Retain adequate existing execution, limits and observability. Missing
baseline data limits a comparative performance claim; it does not block independent reasoning or
already authorized recovery through a validated rollback/drain path.

1. **Keep the relevant baseline.** For a performance comparison, record p50/p99 at the target rate,
   in-flight concurrency, thread counts, heap, connection-pool utilisation and downstream errors.
   Reuse sufficient comparable data; do not invent a baseline or claim improvement without one.
2. **Inventory affected pools and write down what they limit.** Include shared resources and
   callers whose demand changes. For each: how many threads,
   what resource sat behind it, and what happens if that number becomes unbounded. This
   inventory supports the migration decision; include queues, ordering, context and lifecycle ownership.
3. **Audit for blockers** — monitor pinning on JDK 21–23 or retained legacy locking, native/foreign pinning,
   carrier-capturing or file-heavy paths, `ThreadLocal` caches, thread-name dependencies,
   executors that encode ordering.
   The greps are in the playbook.
4. **Preserve required limits before removing the old enforcement.** A separate platform-thread
   deployment can isolate limit-policy risk; an evidenced paired change can be appropriate when
   coexistence changes queue/deadline semantics. Compare predeclared SLO, correctness and overload
   criteria; unchanged throughput is not proof of equivalence.
5. **Change one bounded workload**, using a flag or scoped deployment with owned drain/rollback.
   Choose from actual objective, downstream bounds, blast radius and evidence. Validate the claimed
   behavior under representative demand before widening.
6. **Revalidate the connection pool deliberately**, using measured hold time, required throughput,
   queueing headroom and the database's aggregate capacity — not in proportion to new thread count.
   Keep an adequate pool size; a review need not produce a resize.
7. **Confirm the observability works** on the new model before widening: JSON thread dumps,
   pinning events, scheduler/resource metrics and request correlation as relevant. Names are one
   diagnostic aid, not a mandatory replacement for adequate context and signals.
8. **Widen one workload at a time**, with the rollback criteria stated before each step.

## Rules

- **Prefer staged changes with an observed scope.** A framework flag may affect several
  execution paths while leaving custom executors and client pools unchanged. Inventory which
  paths actually switch; the flag alone neither establishes safe migration nor removes every limit.
- **Every required property of a removed pool needs a replacement before the flip.** Write the pairs
  down: "payment concurrency was limited by request workers → measured provider gate plus
  bounded ingress". Record a justified removal when a limit served no required property.
  Bound waiting tasks as well as active calls, across replicas, retries and fan-out.
- **`newSingleThreadExecutor` and `newFixedThreadPool(1)` are often correctness, not
  performance.** They serialise. Replacing them with per-task virtual threads silently
  removes ordering and mutual exclusion. Find every one and classify it before touching it.
- **Downstream pressure can increase.** Treat that as a hypothesis to measure, not proof of
  success. Respect existing shared-capacity budgets and coordination arrangements before
  increasing aggregate demand.
- **The connection pool is not the thing to grow first.** More concurrent requests do not
  make the database faster; excess concurrency can worsen queueing and timeout rates.
- **Do not expect CPU-bound work to improve.** Virtual threads add no CPU capacity. A request can
  still use one virtual lifetime while CPU-heavy phases are isolated/bounded; migrate only for a
  demonstrated lifecycle/operability reason.
- **Separate waiting capacity from dependency latency.** Cheaper waiting can be a valid objective
  when the dependency has measured headroom and demand remains bounded. It does not make an
  individual slow call faster. More concurrency against a saturated dependency is not a remedy.
- **Check the JDK and effective locking mode before auditing locks.** On JDK 21–23 a virtual thread
  that blocks while holding a monitor can pin. JEP 491 removes monitor-only pinning under default
  HotSpot locking in 24+; nondefault legacy locking can retain it. For example, HotSpot 24 GA's
  `LockingMode=1` retains monitor pinning; verify whether the target still supports and uses that mode.
  Do not rewrite locks or change runtime flags from the version number alone. In JDK 24+
  `-Djdk.tracePinnedThreads` was removed and does nothing; use supported target diagnostics.
  Blocking with a native/foreign frame still present can pin on 24+, including Java callbacks.
- **Keep or introduce bounded platform execution where evidence requires it**: CPU parallelism,
  thread affinity/priority, or causal native/foreign/file paths that the current JDK cannot handle
  efficiently. A migration need not be total.
- **Recheck cancellation against the actual I/O contract.** Interrupting a virtual thread in
  a system-default socket read closes that socket; a read timeout has different semantics.
  Audit pool reuse, shared-resource ownership and retry classification before widening;
  see `references/what-breaks.md`.
- **Rollback should be fast and rehearsed.** A per-workload runtime/configuration switch is ideal;
  where architecture prevents it, staged deployment rollback must still preserve task ordering,
  drain and compatibility.
- **Load-test against the real dependency or a faithful simulator.** The entire mechanism
  being changed is what happens while waiting; a mocked dependency that returns instantly
  removes the phenomenon under test.
- **Compare at the workload's controlled demand.** For open traffic, compare target arrival
  rate/mix and count offered, admitted, useful completions, timeouts and drops. A closed-user
  workload can use fixed population/think time; capacity sweeps can also be useful. Neither
  increased admission nor a faster saturation result alone proves target-rate SLO improvement.

Record the workload/limit inventory, observed baseline and canary outcomes, context/order/cancellation
checks, rollback/drain procedure and unresolved evidence. Keep this proportionate to the changed scope.

## References

- [The staged playbook](references/migration-playbook.md) — the stages with entry and exit
  criteria, the audit commands, the limit-inventory template, canary and rollback criteria,
  and the pool re-sizing arithmetic. Read at the start of the migration and at each stage
  boundary.
- [What breaks quietly](references/what-breaks.md) — the catalogue of behaviours that change
  without an error: thread naming and log correlation, `ThreadLocal` caches, ordering
  guarantees, pool metrics that go to zero, `@Async` and `@Scheduled`, tests that depended on
  a pool. Read during the audit, and again when something inexplicable appears after a flip.
- [JEP 444: Virtual Threads](https://openjdk.org/jeps/444)
- [JEP 491: Synchronize Virtual Threads without Pinning](https://openjdk.org/jeps/491)
- [Java 25 virtual-thread adoption guide](https://docs.oracle.com/en/java/javase/25/core/virtual-threads.html)

Files in this skill

  • SKILL.md8.9 KB
  • references/migration-playbook.md11.3 KB
  • references/what-breaks.md10.5 KB
  • skill.yaml2.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.