[{"data":1,"prerenderedAt":353},["ShallowReactive",2],{"navigation":3,"/blog/laravel-report-generation-30s-to-2s":26,"/blog/laravel-report-generation-30s-to-2s-surround":348},[4],{"title":5,"path":6,"stem":7,"children":8,"page":25},"Blog","/blog","blog",[9,13,17,21],{"title":10,"path":11,"stem":12},"GitHub Actions CI/CD for Laravel: A Zero-Downtime Deployment Setup","/blog/github-actions-laravel-zero-downtime-deployment","blog/github-actions-laravel-zero-downtime-deployment",{"title":14,"path":15,"stem":16},"Integrating LLMs into Laravel Using Python Microservices","/blog/integrating-llms-into-laravel-python-microservices","blog/integrating-llms-into-laravel-python-microservices",{"title":18,"path":19,"stem":20},"How I Cut Report Generation from 30s to 2s in Laravel","/blog/laravel-report-generation-30s-to-2s","blog/laravel-report-generation-30s-to-2s",{"title":22,"path":23,"stem":24},"Why I Chose PostgreSQL Over MongoDB for a CRM Reporting Engine","/blog/postgresql-vs-mongodb-crm-reporting","blog/postgresql-vs-mongodb-crm-reporting",false,{"id":27,"title":18,"author":28,"body":32,"date":340,"description":341,"extension":342,"image":343,"meta":344,"minRead":184,"navigation":162,"path":19,"seo":345,"sitemap":346,"stem":20,"__hash__":347},"blog/blog/laravel-report-generation-30s-to-2s.md",{"name":29,"avatar":30},"Vincent Ndegwa",{"src":31,"alt":29},"/personal/profile.jpeg",{"type":33,"value":34,"toc":332},"minimark",[35,39,42,47,50,80,83,87,90,110,127,130,134,137,193,196,200,203,243,246,250,253,266,270,325,328],[36,37,38],"p",{},"I inherited a Laravel reporting module that looked fine in code review but failed in real life. The monthly report endpoint took 32 seconds on a good day. At month end, when many users hit the button together, request queues piled up and CPU usage climbed into panic territory.",[36,40,41],{},"I did not start with rewrites. I started with measurement.",[43,44,46],"h2",{"id":45},"step-1-profile-before-you-touch-anything","Step 1: Profile Before You Touch Anything",[36,48,49],{},"Laravel Telescope showed the endpoint was firing 47 SQL queries, many inside loops. A few were scanning large transaction tables with weak indexes.",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-php shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","foreach ($departments as $department) {\n    $department->transactions; // one query per row\n}\n","php","",[58,59,60,68,74],"code",{"__ignoreMap":56},[61,62,65],"span",{"class":63,"line":64},"line",1,[61,66,67],{},"foreach ($departments as $department) {\n",[61,69,71],{"class":63,"line":70},2,[61,72,73],{},"    $department->transactions; // one query per row\n",[61,75,77],{"class":63,"line":76},3,[61,78,79],{},"}\n",[36,81,82],{},"This pattern was the main reason latency exploded under load.",[43,84,86],{"id":85},"step-2-remove-n1-and-fix-indexing","Step 2: Remove N+1 and Fix Indexing",[36,88,89],{},"I switched to eager loading and added a composite index on high selectivity columns used in filters.",[51,91,93],{"className":53,"code":92,"language":55,"meta":56,"style":56},"$departments = Department::with([\n    'transactions' => fn ($q) => $q->whereBetween('created_at', [$start, $end])\n])->get();\n",[58,94,95,100,105],{"__ignoreMap":56},[61,96,97],{"class":63,"line":64},[61,98,99],{},"$departments = Department::with([\n",[61,101,102],{"class":63,"line":70},[61,103,104],{},"    'transactions' => fn ($q) => $q->whereBetween('created_at', [$start, $end])\n",[61,106,107],{"class":63,"line":76},[61,108,109],{},"])->get();\n",[51,111,115],{"className":112,"code":113,"language":114,"meta":56,"style":56},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","CREATE INDEX idx_txn_department_created_at\nON transactions (department_id, created_at);\n","sql",[58,116,117,122],{"__ignoreMap":56},[61,118,119],{"class":63,"line":64},[61,120,121],{},"CREATE INDEX idx_txn_department_created_at\n",[61,123,124],{"class":63,"line":70},[61,125,126],{},"ON transactions (department_id, created_at);\n",[36,128,129],{},"Result: query phase dropped from around 18 seconds to around 6 seconds.",[43,131,133],{"id":132},"step-3-move-heavy-generation-to-queues","Step 3: Move Heavy Generation to Queues",[36,135,136],{},"PDF generation and aggregation logic were running in the request cycle. I moved the expensive work to a queued job and made the endpoint return immediately.",[51,138,140],{"className":53,"code":139,"language":55,"meta":56,"style":56},"public function generate(ReportRequest $request)\n{\n    $job = GenerateReportJob::dispatch($request->validated());\n\n    return response()->json([\n        'status' => 'queued',\n        'job_id' => $job->id,\n    ]);\n}\n",[58,141,142,147,152,157,164,170,176,182,188],{"__ignoreMap":56},[61,143,144],{"class":63,"line":64},[61,145,146],{},"public function generate(ReportRequest $request)\n",[61,148,149],{"class":63,"line":70},[61,150,151],{},"{\n",[61,153,154],{"class":63,"line":76},[61,155,156],{},"    $job = GenerateReportJob::dispatch($request->validated());\n",[61,158,160],{"class":63,"line":159},4,[61,161,163],{"emptyLinePlaceholder":162},true,"\n",[61,165,167],{"class":63,"line":166},5,[61,168,169],{},"    return response()->json([\n",[61,171,173],{"class":63,"line":172},6,[61,174,175],{},"        'status' => 'queued',\n",[61,177,179],{"class":63,"line":178},7,[61,180,181],{},"        'job_id' => $job->id,\n",[61,183,185],{"class":63,"line":184},8,[61,186,187],{},"    ]);\n",[61,189,191],{"class":63,"line":190},9,[61,192,79],{},[36,194,195],{},"The frontend now polls a lightweight status endpoint. Users get fast feedback instead of watching a spinner freeze.",[43,197,199],{"id":198},"step-4-cache-expensive-aggregates-in-redis","Step 4: Cache Expensive Aggregates in Redis",[36,201,202],{},"Some report widgets are read hundreds of times but change only when new transactions land. Perfect cache candidates.",[51,204,206],{"className":53,"code":205,"language":55,"meta":56,"style":56},"$summary = Cache::tags(['reports', 'department:' . $departmentId])\n    ->remember('month:' . $month, now()->addMinutes(15), function () use ($departmentId, $month) {\n        return Transaction::where('department_id', $departmentId)\n            ->whereMonth('created_at', $month)\n            ->selectRaw('SUM(amount) AS total, COUNT(*) AS count')\n            ->first();\n    });\n",[58,207,208,213,218,223,228,233,238],{"__ignoreMap":56},[61,209,210],{"class":63,"line":64},[61,211,212],{},"$summary = Cache::tags(['reports', 'department:' . $departmentId])\n",[61,214,215],{"class":63,"line":70},[61,216,217],{},"    ->remember('month:' . $month, now()->addMinutes(15), function () use ($departmentId, $month) {\n",[61,219,220],{"class":63,"line":76},[61,221,222],{},"        return Transaction::where('department_id', $departmentId)\n",[61,224,225],{"class":63,"line":159},[61,226,227],{},"            ->whereMonth('created_at', $month)\n",[61,229,230],{"class":63,"line":166},[61,231,232],{},"            ->selectRaw('SUM(amount) AS total, COUNT(*) AS count')\n",[61,234,235],{"class":63,"line":172},[61,236,237],{},"            ->first();\n",[61,239,240],{"class":63,"line":178},[61,241,242],{},"    });\n",[36,244,245],{},"On transaction writes, I invalidate only related tags so fresh data appears quickly without nuking the whole cache.",[43,247,249],{"id":248},"step-5-guardrails-for-scale","Step 5: Guardrails for Scale",[36,251,252],{},"Performance wins disappear without guardrails. I added:",[254,255,256,260,263],"ul",{},[257,258,259],"li",{},"slow query logging",[257,261,262],{},"a threshold alert for report queue wait time",[257,264,265],{},"basic load tests in CI before each release",[43,267,269],{"id":268},"outcome","Outcome",[271,272,273,289],"table",{},[274,275,276],"thead",{},[277,278,279,283,286],"tr",{},[280,281,282],"th",{},"Metric",[280,284,285],{},"Before",[280,287,288],{},"After",[290,291,292,304,314],"tbody",{},[277,293,294,298,301],{},[295,296,297],"td",{},"Report generation",[295,299,300],{},"32s",[295,302,303],{},"1.8s",[277,305,306,309,311],{},[295,307,308],{},"API response time",[295,310,300],{},[295,312,313],{},"\u003C 200ms",[277,315,316,319,322],{},[295,317,318],{},"Month end CPU",[295,320,321],{},"~90%",[295,323,324],{},"~20%",[36,326,327],{},"The important lesson: serious performance tuning is usually less about heroics and more about disciplined bottleneck removal. Measure, target one class of failure at a time, and verify with numbers.",[329,330,331],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":56,"searchDepth":70,"depth":70,"links":333},[334,335,336,337,338,339],{"id":45,"depth":70,"text":46},{"id":85,"depth":70,"text":86},{"id":132,"depth":70,"text":133},{"id":198,"depth":70,"text":199},{"id":248,"depth":70,"text":249},{"id":268,"depth":70,"text":269},"2025-04-23","A practical walkthrough of how Redis caching, Laravel Queues, and careful query optimisation transformed a sluggish ERP reporting module into a near-instant one.","md","https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=1260&auto=format&fit=crop&q=80",{},{"title":18,"description":341},{"loc":19},"ERrz3UHmqhL-sBuNsCUFxXDzNVEcG2U5ozallnjRF28",[349,351],{"title":14,"path":15,"stem":16,"description":350,"children":-1},"A practical guide to wiring a LangChain Python service to a Laravel backend so you can add AI-powered features without abandoning your existing stack.",{"title":22,"path":23,"stem":24,"description":352,"children":-1},"Exploring how strategic database choices impact query complexity, reporting accuracy, and long-term maintainability — and why relational won for this use case.",1791651066261]