{"id":1560,"date":"2025-10-17T10:00:16","date_gmt":"2025-10-17T07:00:16","guid":{"rendered":"https:\/\/www.drinkits.lv\/?p=1560"},"modified":"2025-10-17T16:55:43","modified_gmt":"2025-10-17T13:55:43","slug":"forges-5-minute-problem-how-i-built-a-blazing-fast-jira-cloud-attachment-scanner","status":"publish","type":"post","link":"https:\/\/www.drinkits.lv\/en\/2025\/10\/17\/forges-5-minute-problem-how-i-built-a-blazing-fast-jira-cloud-attachment-scanner\/","title":{"rendered":"Forge&#8217;s 5-Minute Problem: How I Built a Blazing Fast Jira Cloud Attachment Scanner"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" class=\"alignright wp-image-1566 size-medium\" src=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/attachment_architect-300x191.png\" alt=\"\" width=\"300\" height=\"191\" srcset=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/attachment_architect-300x191.png 300w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/attachment_architect-768x490.png 768w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/attachment_architect-18x12.png 18w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/attachment_architect-700x447.png 700w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/attachment_architect.png 997w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/>When I set out to build Attachment Architect, the main obstacle was clear: how do you scan a Jira instance with over 100,000 issues on a serverless platform designed with timeout constraints? The standard Atlassian Forge <code>scheduledTrigger<\/code> runs only every five minutes. A rough calculation indicated it would take <strong>nearly two days<\/strong> to completely scan a large instance. This wasn&#8217;t a product; it was a science experiment. This is the story of how I abandoned the standard method, navigated Forge&#8217;s restrictions, and built a hybrid scanning engine that is <strong>more than 20 times faster<\/strong>, turning a two-day marathon into an 2-hour sprint.<\/p>\n<h2>The Initial Failure: The Naive <em>scheduledTrigger <\/em>Approach<\/h2>\n<p>All Forge developers start at this point. My initial prototype used a basic scheduled trigger that would run every 5 minutes, process a tiny batch of issues, and save its state. While it was dependable, it was unimaginably slow.<\/p>\n<p><em><!-- This diagram shows the slow, intermittent process. --><\/em><\/p>\n<pre><code>\r\n+----------------------+\r\n| Trigger 1 (10:00 AM) | --- Processes Batch 1 ---&gt; Sleeps for 5 mins\r\n+----------------------+\r\n\r\n+----------------------+\r\n| Trigger 2 (10:05 AM) | --- Processes Batch 2 ---&gt; Sleeps for 5 mins\r\n+----------------------+\r\n\r\n+----------------------+\r\n| Trigger 3 (10:10 AM) | --- Processes Batch 3 ---&gt; ...and so on.\r\n+----------------------+\r\n<\/code><\/pre>\n<p>The result: an issue processing rate of just ~40 issues\/minute. The complete scan of 100,000 issues was expected to take about <strong>42 hours<\/strong>.<\/p>\n<p>I knew this was unacceptable. The user experience would be terrible. Nobody is going to wait two days for their initial results. I had to find a method to make the process continuous rather than intermittent.<\/p>\n<h2>The Breakthrough: The Self-Invoking Webhook Pattern<\/h2>\n<p>Then, an amazing thing happened. I stopped depending on Forge to \u201cwake up\u201d my app every 5 minutes and instead, made my app \u201cwake itself up\u201d right after each batch.<\/p>\n<h4>Here is the process:<\/h4>\n<ol>\n<li>The user clicks &#8220;Start Scan.&#8221;<\/li>\n<li>This action calls a Forge resolver that processes the first batch.<\/li>\n<li>When Batch 1 is fully processed, instead of stopping, it makes a <code>fetch<\/code> call to its own <code>webTrigger<\/code> URL, in a way saying to itself: &#8220;I am finished, process the next batch <em>right now<\/em>.&#8221;<\/li>\n<li>The <code>webTrigger<\/code> then takes care of Batch 2, and when it\u2019s done, it calls its own API once again.<\/li>\n<li>This results in a fast, uninterrupted processing chain that is only limited by the Jira API, effectively ignoring the 5-minute <code>scheduledTrigger<\/code> restriction.<\/li>\n<\/ol>\n<p>The implementation of this technique resulted in a reduction in the scanning time for 100,000 issues from 42 hours to <strong>just over 2 hours<\/strong>.<\/p>\n<p><em><!-- This diagram shows the high-speed, continuous process. --><\/em><\/p>\n<pre><code>\r\n      [User Clicks 'Start Scan']\r\n               |\r\n               v\r\n+--------------------------------+\r\n| Resolver (Processes Batch 1)   | --[fetch]--&gt; (Calls its own Web Trigger)\r\n+--------------------------------+       ^\r\n               |                         |\r\n        (Updates UI)                     |\r\n                                         |\r\n+--------------------------------+       |\r\n| Web Trigger (Processes Batch 2)| --[fetch]--&gt; (Calls itself again)\r\n+--------------------------------+       ^\r\n               |                         |\r\n         (Saves state)                   |\r\n                                         |\r\n+--------------------------------+       |\r\n| Web Trigger (Processes Batch 3)| --[fetch]---- ... and so on\r\n+--------------------------------+\r\n<\/code><\/pre>\n<h2>Adding a Safety Net: The Hybrid Engine<\/h2>\n<p><a href=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/Ekranuznemums-2025-10-16-162032.png\" data-lbwps-width=\"614\" data-lbwps-height=\"804\" data-lbwps-srcsmall=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/Ekranuznemums-2025-10-16-162032-9x12.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignleft wp-image-1565 size-medium\" src=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/Ekranuznemums-2025-10-16-162032-229x300.png\" alt=\"\" width=\"229\" height=\"300\" srcset=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/Ekranuznemums-2025-10-16-162032-229x300.png 229w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/Ekranuznemums-2025-10-16-162032-9x12.png 9w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/Ekranuznemums-2025-10-16-162032-535x700.png 535w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/10\/Ekranuznemums-2025-10-16-162032.png 614w\" sizes=\"auto, (max-width: 229px) 100vw, 229px\" \/><\/a>But what if the user closes their browser? The &#8220;active&#8221; webhook chain might halt. At this point, I devised a hybrid system that would provide the best of both worlds.<\/p>\n<p>The idea was to use the ultra-fast webhook method when the user was actively watching, but to have the reliable <code>scheduledTrigger<\/code> act as a &#8220;watchdog&#8221; that would seamlessly take over if the active process stopped for any reason.<\/p>\n<h4>How it Works (Simplified):<\/h4>\n<ul>\n<li><strong>Frontend Active Mode:<\/strong> While the dashboard is open, it pings a backend function every second, which through the webhook chain triggers the next batch.<\/li>\n<li><strong>Scheduled Trigger &#8220;Watchdog&#8221;:<\/strong> The old 5-minute trigger still runs. Its first task is to check a timestamp: &#8220;When was the last batch processed?&#8221;. If it was less than 2 minutes ago, it means the active mode is running, so the watchdog does nothing. If it was more than 2 minutes ago, it means the active process has stopped, and the watchdog automatically takes over, processing batches in the slow but guaranteed background mode.<\/li>\n<\/ul>\n<p><em><!-- Insert your hybrid system flowchart image here --><\/em><\/p>\n<h2>Conclusion: Lessons I Learned for Forge Developers<\/h2>\n<p>This journey taught me some very important lessons about building high-performance, enterprise-grade apps on Forge:<\/p>\n<ol>\n<li><strong>Don&#8217;t Fear the Limits, Embrace Them:<\/strong> I understood that the limitations of Forge (like the 25-second function timeout and 5-minute triggers) are not obstacles; they are boundaries that compel you to write better, more robust, and more scalable serverless code.<\/li>\n<li><strong>Build a Resilient, Hybrid System:<\/strong> The combination of real-time and scheduled processes gives you the advantage of both speed and reliability.<\/li>\n<\/ol>\n<p>By sharing my technical journey, I hope to help other developers in the Atlassian ecosystem build even more amazing things. And if you wish to see this engine working, feel free to go to the Marketplace and check out <strong>Attachment Architect<\/strong>.<\/p>\n<p><strong><a href=\"https:\/\/marketplace.atlassian.com\/apps\/2464899201\/attachment-architect\" target=\"_blank\" rel=\"noopener noreferrer\">See Attachment Architect on the Atlassian Marketplace<\/a><\/strong><\/p>\n<h2>Beyond the Scanner: What\u2019s Next for Attachment Architect?<\/h2>\n<p>Building a high-performance scanning engine was just the first, critical step. The true vision for Attachment Architect is to create a complete, intelligent governance platform. Here\u2019s a sneak peek at what I\u2019m building next:<\/p>\n<ul>\n<li><strong>Custom Scan Scopes:<\/strong> Soon, you&#8217;ll be able to run scans not just on your entire instance, but on a <strong>specific project<\/strong> or even based on a <strong>custom JQL query<\/strong>. This will allow for incredibly fast and highly-targeted audits.<\/li>\n<li><strong>Automated Governance Policies:<\/strong> The next major leap is moving from analysis to action. I\u2019m building a powerful policy engine that will let you create <strong>&#8220;set-it-and-forget-it&#8221; rules<\/strong> to automatically manage new duplicates and enforce data retention policies.<\/li>\n<li><strong>Deep Content Security Scanning:<\/strong> The ultimate goal is to help you find hidden risks. The upcoming security module will scan the content of your files for potential secrets, like <strong>exposed API keys or Personal Identifiable Information (PII)<\/strong>, transforming your attachment library from a blind spot into a governable asset.<\/li>\n<\/ul>","protected":false},"excerpt":{"rendered":"<a href=\"https:\/\/www.drinkits.lv\/en\/2025\/10\/17\/forges-5-minute-problem-how-i-built-a-blazing-fast-jira-cloud-attachment-scanner\/\" rel=\"bookmark\" title=\"Permalink to Forge&#8217;s 5-Minute Problem: How I Built a Blazing Fast Jira Cloud Attachment Scanner\"><p>When I set out to build Attachment Architect, the main obstacle was clear: how do you scan a Jira instance with over 100,000 issues on a serverless platform designed with timeout constraints? The standard Atlassian Forge scheduledTrigger runs only every five minutes. A rough calculation indicated it would take nearly two days to completely scan [&hellip;]<\/p>\n<\/a>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[245],"tags":[269,266,246,48,249,267,271,273,270,272,268],"class_list":{"0":"post-1560","1":"post","2":"type-post","3":"status-publish","4":"format-standard","6":"category-jira","7":"tag-app-development","8":"tag-atlassian-forge","9":"tag-drinkits-dev","10":"tag-jira-api","11":"tag-jira-cloud","12":"tag-performance","13":"tag-scheduled-trigger","14":"tag-scheduledtrigger","15":"tag-serverless","16":"tag-tutorial","17":"tag-webhook","18":"h-entry","19":"hentry"},"_links":{"self":[{"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/posts\/1560","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/comments?post=1560"}],"version-history":[{"count":13,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/posts\/1560\/revisions"}],"predecessor-version":[{"id":1578,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/posts\/1560\/revisions\/1578"}],"wp:attachment":[{"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/media?parent=1560"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/categories?post=1560"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/tags?post=1560"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}