{"id":30115,"date":"2025-07-01T17:33:25","date_gmt":"2025-07-01T12:03:25","guid":{"rendered":"https:\/\/blogrevamp.cashfree.com\/?p=30115"},"modified":"2025-07-01T17:34:12","modified_gmt":"2025-07-01T12:04:12","slug":"java-21-virtual-threads-lessons-production","status":"publish","type":"post","link":"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/","title":{"rendered":"7 Key Lessons from Using Java 21 Virtual Threads in Production"},"content":{"rendered":"<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_81 counter-hierarchy ez-toc-counter ez-toc-custom ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #005c31;color:#005c31\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #005c31;color:#005c31\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/#Using_Virtual_Threads_for_CPU-Intensive_Workloads\" >Using Virtual Threads for CPU-Intensive Workloads<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/#Blocking_Operations_That_Can_Pin_Carrier_Threads\" >Blocking Operations That Can Pin Carrier Threads<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/#Not_Adjusting_Heap_Memory_for_Virtual_Threads\" >Not Adjusting Heap Memory for Virtual Threads<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/#Using_Virtual_Threads_with_Traditional_Thread_Pool_Executors\" >Using Virtual Threads with Traditional Thread Pool Executors<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/#Misusing_ThreadLocals_in_Virtual_Threads\" >Misusing ThreadLocals in Virtual Threads<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/#Overloading_Virtual_Threads_with_Excessive_Tasks\" >Overloading Virtual Threads with Excessive Tasks<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/#Smarter_Approach\" >Smarter Approach:\u00a0<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/#Ignoring_Virtual_Thread_Monitoring_and_Debugging\" >Ignoring Virtual Thread Monitoring and Debugging<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/#Final_Takeaways\" >Final Takeaways<\/a><\/li><\/ul><\/nav><\/div>\n\n<p class=\"wp-block-paragraph\">Virtual Threads, one of the most talked about features in Java 21. They are way more lightweight than traditional platform threads and let you handle high concurrency with less resource usage. At Cashfree Payments, we recently migrated a few of our microservices to Java 21 and began integrating <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/21\/core\/virtual-threads.html#GUID-15BDB995-028A-45A7-B6E2-9BA15C2E0501\">Virtual Threads<\/a> for I\/O-heavy operations like bank API calls.&nbsp; We quickly learned that using Virtual Threads the wrong way can silently erode performance, or worse, introduce instability. This post is a reflection of that journey.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These are seven lessons, including mistakes, anti-patterns, and edge cases, we ran into (or deliberately avoided) while working with Virtual Threads. Whether you\u2019re just getting started or already knee-deep in migration, these seven lessons can save you from expensive surprises.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s dive in.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Using_Virtual_Threads_for_CPU-Intensive_Workloads\"><\/span><strong>Using Virtual Threads for CPU-Intensive Workloads<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Virtual Threads are optimised for I\/O-bound tasks but not recommended for CPU-intensive tasks. Unlike <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/21\/core\/virtual-threads.html#GUID-2BCFC2DD-7D84-4B0C-9222-97F9C7C6C521\">Platform Threads<\/a>, they do not have OS-level thread scheduling leverage, which might give you suboptimal CPU utilisation. Moreover, in CPU-intensive workloads, Virtual Threads can face starvation due to the unavailability of underneath carrier threads.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example of incorrect usage:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em><strong>ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();\nfor (int i = 0; i &lt; 500; i++) {\n\u00a0\u00a0\u00a0\u00a0executor.submit(() -> {\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0long sum = 0;\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0for (long j = 0; j &lt; 1_000_000_000; j++) {\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0sum += j;\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0System.out.println(sum);\n\u00a0\u00a0\u00a0\u00a0});\n}<\/strong><\/em><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Evaluation:<\/strong> First things first, before applying Virtual Threads, check the nature of the workload. If it is an I\/O bound task (tasks spend most of their time waiting), then only prefer Virtual Threads. For our use case, we mostly have bank-related network calls with high traffic and moderate latency (i.e. 2-5s, which is very high in CPU clock). That means our workload is <strong>I\/O-Intensive <\/strong>with high concurrency, suitable for Virtual Thread.<br><br><strong>Smarter approach: <\/strong>For CPU-heavy tasks, use a fixed-size thread pool instead. The recommended pool size should be equal to the number of available cores.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><strong><em>ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());<\/em><\/strong><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Blocking_Operations_That_Can_Pin_Carrier_Threads\"><\/span><strong>Blocking Operations That Can Pin Carrier Threads<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Certain operations can <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/21\/core\/virtual-threads.html#GUID-704A716D-0662-4BC7-8C7F-66EE74B1EDAD\">pin a Virtual Thread<\/a> to a carrier thread (Carrier threads are known as those Platform Threads that execute virtual threads), due to which other Virtual Threads can be blocked from executing, even in <strong>I\/O-Intensive Workload<\/strong>s. This loses the benefits of Virtual Threads.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Common blocking operations that cause pinning:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>synchronised blocks<\/li>\n\n\n\n<li>Object.wait()<\/li>\n\n\n\n<li>Native method calls<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example of incorrect usage:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em><strong>synchronised (lock) {\n\u00a0\u00a0\u00a0\u00a0Thread.sleep(5000); \/\/ This pins the Virtual Thread to an OS thread\n}<\/strong><\/em><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Evaluation: <\/strong>Before switching to Virtual Threads, check if there are any blocking operations highlighted above in your codebase. If you find any, either try to eliminate them or use non-blocking alternatives. In our case, we didn&#8217;t have any obvious blocking calls. But even then, we were not 100% sure that the whole code was free of them since some third-party libraries might still be using blocking operations behind the scenes. We took the best approach here to <strong>try <\/strong>and<strong> test<\/strong>. We haven\u2019t found anything suspicious related to performance degradation. Still, if you notice some unnecessary timeouts or the number of live Platform Threads is going up significantly, then there is a probability that some carrier threads are getting stuck. To track this down, you can use the JVM flag -Djdk.tracePinnedThreads=full, which helps you pinpoint the problem.<br><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Smarter approach:<\/strong> Either avoid synchronised in Virtual Threads or use alternative concurrency mechanisms like ReentrantLock. Java 24 has improvements to reduce pinning in synchronised blocks.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Not_Adjusting_Heap_Memory_for_Virtual_Threads\"><\/span><strong>Not Adjusting Heap Memory for Virtual Threads<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Platform threads store their stacks in native memory (refers to overall system memory), whereas Virtual Threads store their stacks in heap memory (refers to a specific region within the <a href=\"https:\/\/docs.oracle.com\/javase\/specs\/jvms\/se8\/html\/\">Java Virtual Machine<\/a> (JVM), dedicated to storing Java objects and managed by JVM <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/11\/gctuning\/introduction-garbage-collection-tuning.html#GUID-8A443184-7E07-4B71-9777-4F12947C8184\">Garbage Collector<\/a>). If the heap size is not properly configured, Virtual Threads may lead to <strong>lower concurrency<\/strong> than Platform Threads. In some cases, even a small number of Virtual Threads can cause an <strong>OutOfMemoryError<\/strong>, depending upon the workload. Resulting in reduced concurrency instead of improving it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Evaluation:&nbsp;<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/lh7-rt.googleusercontent.com\/docsz\/AD_4nXcZgQgHUhpYurtVr8jmke_YyEjGTJgF-hkBFfx5S0o2EbsupR7hOUdLm8U0OWqzaeboUyywwQii6RwbnLVzzbPdN1liI4lqzATZD6h1yF_Hlycauynf6FtidELOZqCEFbokDyNMjQ?key=sxjau74NUr0ZHEIS0c2-ZQ\" alt=\"\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The graph above shows the production heap usage of one of our Cashfree microservices that runs on <a href=\"https:\/\/docs.spring.io\/spring-boot\/index.html\">Spring Boot<\/a> + Java 21. The graph is split into two parts by the black vertical line. On the left, you can see heap memory usage before we started using Virtual Threads in place of <a href=\"https:\/\/tomcat.apache.org\/tomcat-8.0-doc\/api\/org\/apache\/tomcat\/util\/threads\/package-summary.html\">Tomcat Threads<\/a>, and on the right, after the switch. As you can observe, there&#8217;s a significant increase in heap memory usage after moving to Virtual Threads, which was very obvious. This ends up impacting the heap availability for actual traffic. An increased heap does not mean that overall memory consumption increases in the case of Virtual Threads. Instead, it means that the stack which used to take space in native memory is now shifted to the heap.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Smarter approach:<\/strong> Adjust JVM options to allocate more heap memory to achieve high concurrency. We did the same for our microservice.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><strong><em>java -XX:MaxRAMPercentage=35.0 -jar myapp.jar<\/em><\/strong><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><br>This sets the max heap limit to 35% of available RAM.<br><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Using_Virtual_Threads_with_Traditional_Thread_Pool_Executors\"><\/span><strong>Using Virtual Threads with Traditional Thread Pool Executors<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> You might create unnecessary scheduling overhead if Virtual Threads are used with traditional thread pool executors (like FixedThreadPool). Submitting Virtual Threads to such a thread pool <strong>double-schedules<\/strong> them:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>First, the <strong>Platform Thread pool schedules the task<\/strong>.<\/li>\n\n\n\n<li>Then, inside the task, a <strong>Virtual Thread is scheduled again<\/strong> by the JVM.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example of incorrect usage:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em><strong>ExecutorService executor = Executors.newFixedThreadPool(100);\nfor (int i = 0; i &lt; 1000; i++) {\n\u00a0\u00a0\u00a0\u00a0executor.submit(() -> Thread.startVirtualThread(() -> System.out.println(\"Hello\")));\n}<\/strong><\/em><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Evaluation: <\/strong>If you want to convert your thread pool to use Virtual Thread, it doesn\u2019t mean submitting Virtual Threads to the pool. This will create unnecessary overhead, as mentioned earlier. Instead, you must take a different approach: don\u2019t pool them. The nature of a Virtual Thread is to be created, processed, and destroyed quickly. The idea is to use a Virtual Thread per task and then let it destroy once finished. A better approach is already mentioned below. But before switching to a new executor, keep in mind that you have now unlocked a ton of concurrency, which might become an issue for any resources your tasks depend on. We&#8217;ll dive into that in point #6 later.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Smarter approach:<\/strong> Use a special executor built for Virtual Thread with same analogy that is discussed above:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();<br><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Misusing_ThreadLocals_in_Virtual_Threads\"><\/span><strong>Misusing ThreadLocals in Virtual Threads<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Using <a href=\"https:\/\/docs.oracle.com\/javase\/8\/docs\/api\/java\/lang\/ThreadLocal.html\">ThreadLocal<\/a> for caching in a virtual thread context can backfire. Unlike traditional platform threads, virtual threads are short-lived and not reused. This actually means every time you spawn a new virtual thread, it gets its own fresh ThreadLocal state. So, even if you\u2019ve cached something expensive, the next thread won\u2019t see it as it starts from scratch.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s an example that highlights the issue:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example of incorrect usage:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><strong><em>private static final ThreadLocal&lt;Integer> expensiveObjectCache = ThreadLocal.withInitial(() -> null);\n\nExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();\n\nfor (int i = 0; i &lt; 10; i++) {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0final int taskId = i;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0executor.submit(() -> {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Integer result = expensiveObjectCache.get();\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if (result == null) { \/\/ If cache is empty, compute the value (simulate computation)\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0result = expensiveComputation(taskId);\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0expensiveObjectCache.set(result);\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0System.out.println(\"Task \" + taskId + \" computed and cached result: \" + result);\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} else {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0System.out.println(\"Task \" + taskId + \" used cached result: \" + result);\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\n\u00a0\u00a0\u00a0});\n}\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0executor.close();\n\u00a0\u00a0\u00a0\u00a0}<\/em><\/strong><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Evaluation: <\/strong>Check for Thread Local used in the code. You might find two types of usage of Thread Local:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Context-specific information<\/strong>: This is when you store things like a user object in ThreadLocal to avoid passing it around in every method. In such cases, using Thread Local with Virtual Thread, you must clean up the ThreadLocal using remove() in a \u2018finally\u2019 block, or you&#8217;ll risk a memory leak.<\/li>\n\n\n\n<li><strong>Caching<\/strong>: While some developers use ThreadLocal to cache expensive objects, this approach falls flat with Virtual Threads. Unlike Platform Threads, Virtual Threads aren\u2019t reused. The former are short-lived and lightweight. As a result, caching via ThreadLocal provides no real benefit.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Smarter approach: <\/strong>There is not one general alternative to offer, but in the above case, a better direction would be to use <strong>ExecutorService executor = Executors.newFixedThreadPool(4);<\/strong> so that expensive objects can be reused across the threads. Using this approach, you only need to compute once per pool thread, cached.<br><br>You should also consider exploring <strong>Scoped Values (introduced in Java 21)<\/strong> instead of Thread Local for a temporary state (<a href=\"https:\/\/openjdk.org\/jeps\/446\">reference<\/a>).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Overloading_Virtual_Threads_with_Excessive_Tasks\"><\/span><strong>Overloading Virtual Threads with Excessive Tasks<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Even though the JVM can handle millions of virtual threads, throwing that kind of concurrency at your system without guardrails is asking for trouble. Here\u2019s where things can go south:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Memory Pressure<\/strong>: Sure, each virtual thread has a tiny stack, around 1\u20132 KB, but that adds up <em>fast<\/em> when you scale into the millions. Suddenly, your heap can start sweating.<br><\/li>\n\n\n\n<li><strong>CPU Starvation<\/strong>: Running too many tasks at the same time can overload the CPU and cause a lot of context switching.<br><\/li>\n\n\n\n<li><strong>Unbounded I\/O Load<\/strong>: When tasks talk to databases or call other services, too many virtual threads can easily overload them<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example of incorrect usage:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><strong><em>ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();\n\nfor (int i = 0; i &lt; 10_000_000; i++) {\n\n\u00a0\u00a0\u00a0\u00a0executor.submit(() -> {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0callDatabase();\n\n\u00a0\u00a0\u00a0\u00a0});\n\n}<\/em><\/strong><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Evaluation: <\/strong>Whenever you introduce virtual threads into your application, always remember: you&#8217;re essentially handing out an unlimited pass to spawn unlimited concurrent tasks. That sounds powerful, but without control, it can overwhelm both your internal resources (like memory or CPU) and external systems (like databases or APIs). So if you want a limit on concurrency, then you shouldn\u2019t depend on executors anymore. Instead, take control of concurrency yourself. Java also gives you dedicated built-in options for this.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Smarter_Approach\"><\/span><strong>Smarter Approach:\u00a0<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you&#8217;re spawning virtual threads in an unbounded loop, either throttle them or use backpressure mechanism to only allow as much concurrency as your system can handle gracefully. Below I have used Semaphore of the same.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><strong><em>Semaphore semaphore = new Semaphore(1000);\n\nExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();\n\nfor (int i = 0; i &lt; 10_000_000; i++) {\n\n\u00a0\u00a0\u00a0\u00a0semaphore.acquire();\n\n\u00a0\u00a0\u00a0\u00a0executor.submit(() -> {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0try {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0callDatabase();\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} finally {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0semaphore.release();\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0\u00a0\u00a0});\n\n}<\/em><\/strong><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Ignoring_Virtual_Thread_Monitoring_and_Debugging\"><\/span><strong>Ignoring Virtual Thread Monitoring and Debugging<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Unlike platform threads, however, virtual threads won&#8217;t appear cleanly in traditional monitoring tools like jstack, top, or basic thread dumps. This lack of visibility makes debugging and performance tuning challenging. One common misconception teams make regarding virtual threads is assuming they&#8217;d give out a performance boost, like magic, without measuring real-world workloads. However, issues like excessive heap usage or thread starvation can go unnoticed without using profiling tools.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Smarter approach:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use Java Flight Recorder (JFR) or Async Profiler to track Virtual Threads.<\/li>\n\n\n\n<li>Enable detailed thread tracking with JVM flags:<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">-Djdk.tracePinnedThreads=full<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Final_Takeaways\"><\/span><strong>Final Takeaways<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Virtual threads are powerful, but just like any other tool, they need to be used with caution. They do well in I\/O-bound use cases, but they\u2019re not a silver bullet. If you jump in without having an understanding of how they behave, especially under stress, you might end up with slower performance, memory issues, or tricky debugging problems.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At Cashfree Payments, we\u2019ve seen real impact using them for high-latency tasks like bank API calls. But we\u2019ve also learned that Virtual Threads need a different mindset. You can\u2019t just replace old threads and expect magic. You need to know where they fit, where they don\u2019t, and how to keep things in check. Bottom line: Virtual Threads are powerful but only when used thoughtfully.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Watch out for this space for more engineering insights!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Virtual Threads, one of the most talked about features in Java 21. They are way more lightweight than traditional platform threads and let you handle high concurrency with less resource usage. At Cashfree Payments, we recently migrated a few of our microservices to Java 21 and began integrating Virtual Threads for I\/O-heavy operations like bank<\/p>\n","protected":false},"author":136,"featured_media":30121,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_exactmetrics_skip_tracking":false,"_exactmetrics_sitenote_active":false,"_exactmetrics_sitenote_note":"","_exactmetrics_sitenote_category":0,"_themeisle_gutenberg_block_has_review":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_wpcom_ai_launchpad_first_post":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1497],"tags":[],"class_list":["post-30115","post","type-post","status-publish","format-standard","has-post-thumbnail","category-engineering"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>7 Key Lessons from Using Java 21 Virtual Threads in Production<\/title>\n<meta name=\"description\" content=\"We migrated key microservices at Cashfree Payments to Java 21 with Virtual Threads for I\/O-heavy tasks. Here are 7 real-world lessons, mistakes, and performance pitfalls to avoid when using Virtual Threads in production.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"7 Key Lessons from Using Java 21 Virtual Threads in Production\" \/>\n<meta property=\"og:description\" content=\"We migrated key microservices at Cashfree Payments to Java 21 with Virtual Threads for I\/O-heavy tasks. Here are 7 real-world lessons, mistakes, and performance pitfalls to avoid when using Virtual Threads in production.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/\" \/>\n<meta property=\"og:site_name\" content=\"Cashfree Payments Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-07-01T12:03:25+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-07-01T12:04:12+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/i0.wp.com\/blogrevamp.cashfree.com\/wp-content\/uploads\/2025\/07\/Aman-Sidhartha_Blog-Tn.png?fit=1200%2C800&ssl=1\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"800\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Aman Siddhartha\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Aman Siddhartha\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"8 minutes\" \/>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"7 Key Lessons from Using Java 21 Virtual Threads in Production","description":"We migrated key microservices at Cashfree Payments to Java 21 with Virtual Threads for I\/O-heavy tasks. Here are 7 real-world lessons, mistakes, and performance pitfalls to avoid when using Virtual Threads in production.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/","og_locale":"en_US","og_type":"article","og_title":"7 Key Lessons from Using Java 21 Virtual Threads in Production","og_description":"We migrated key microservices at Cashfree Payments to Java 21 with Virtual Threads for I\/O-heavy tasks. Here are 7 real-world lessons, mistakes, and performance pitfalls to avoid when using Virtual Threads in production.","og_url":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/","og_site_name":"Cashfree Payments Blog","article_published_time":"2025-07-01T12:03:25+00:00","article_modified_time":"2025-07-01T12:04:12+00:00","og_image":[{"width":1200,"height":800,"url":"https:\/\/i0.wp.com\/blogrevamp.cashfree.com\/wp-content\/uploads\/2025\/07\/Aman-Sidhartha_Blog-Tn.png?fit=1200%2C800&ssl=1","type":"image\/png"}],"author":"Aman Siddhartha","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Aman Siddhartha","Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/#article","isPartOf":{"@id":"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/"},"author":{"name":"Aman Siddhartha","@id":"https:\/\/blogrevamp.cashfree.com\/#\/schema\/person\/3cff87c7f6653b22fc800ab704f8462a"},"headline":"7 Key Lessons from Using Java 21 Virtual Threads in Production","datePublished":"2025-07-01T12:03:25+00:00","dateModified":"2025-07-01T12:04:12+00:00","mainEntityOfPage":{"@id":"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/"},"wordCount":1717,"commentCount":0,"image":{"@id":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/#primaryimage"},"thumbnailUrl":"https:\/\/i0.wp.com\/blogrevamp.cashfree.com\/wp-content\/uploads\/2025\/07\/Aman-Sidhartha_Blog-Tn.png?fit=1200%2C800&ssl=1","articleSection":["Engineering"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/blogrevamp.cashfree.com\/java-21-virtual-threads-lessons-production\/","url":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/","name":"7 Key Lessons from Using Java 21 Virtual Threads in Production","isPartOf":{"@id":"https:\/\/blogrevamp.cashfree.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/#primaryimage"},"image":{"@id":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/#primaryimage"},"thumbnailUrl":"https:\/\/i0.wp.com\/blogrevamp.cashfree.com\/wp-content\/uploads\/2025\/07\/Aman-Sidhartha_Blog-Tn.png?fit=1200%2C800&ssl=1","datePublished":"2025-07-01T12:03:25+00:00","dateModified":"2025-07-01T12:04:12+00:00","author":{"@id":"https:\/\/blogrevamp.cashfree.com\/#\/schema\/person\/3cff87c7f6653b22fc800ab704f8462a"},"description":"We migrated key microservices at Cashfree Payments to Java 21 with Virtual Threads for I\/O-heavy tasks. Here are 7 real-world lessons, mistakes, and performance pitfalls to avoid when using Virtual Threads in production.","breadcrumb":{"@id":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/#primaryimage","url":"https:\/\/i0.wp.com\/blogrevamp.cashfree.com\/wp-content\/uploads\/2025\/07\/Aman-Sidhartha_Blog-Tn.png?fit=1200%2C800&ssl=1","contentUrl":"https:\/\/i0.wp.com\/blogrevamp.cashfree.com\/wp-content\/uploads\/2025\/07\/Aman-Sidhartha_Blog-Tn.png?fit=1200%2C800&ssl=1","width":1200,"height":800},{"@type":"BreadcrumbList","@id":"https:\/\/www.cashfree.com\/blog\/java-21-virtual-threads-lessons-production\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/blogrevamp.cashfree.com\/"},{"@type":"ListItem","position":2,"name":"Engineering","item":"https:\/\/blogrevamp.cashfree.com\/category\/engineering\/"},{"@type":"ListItem","position":3,"name":"7 Key Lessons from Using Java 21 Virtual Threads in Production"}]},{"@type":"WebSite","@id":"https:\/\/blogrevamp.cashfree.com\/#website","url":"https:\/\/blogrevamp.cashfree.com\/","name":"Cashfree Payments Blog","description":"Cashfree Payments- Payment Gateway for India","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/blogrevamp.cashfree.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/blogrevamp.cashfree.com\/#\/schema\/person\/3cff87c7f6653b22fc800ab704f8462a","name":"Aman Siddhartha","url":"https:\/\/blogrevamp.cashfree.com\/author\/aman-siddhartha\/"}]}},"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p9MjXo-7PJ","jetpack_likes_enabled":false,"jetpack_featured_media_url":"https:\/\/i0.wp.com\/blogrevamp.cashfree.com\/wp-content\/uploads\/2025\/07\/Aman-Sidhartha_Blog-Tn.png?fit=1200%2C800&ssl=1","_links":{"self":[{"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/posts\/30115","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/users\/136"}],"replies":[{"embeddable":true,"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/comments?post=30115"}],"version-history":[{"count":7,"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/posts\/30115\/revisions"}],"predecessor-version":[{"id":30125,"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/posts\/30115\/revisions\/30125"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/media\/30121"}],"wp:attachment":[{"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/media?parent=30115"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/categories?post=30115"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogrevamp.cashfree.com\/wp-json\/wp\/v2\/tags?post=30115"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}