When I joined the Ironforge project, the platform was in a difficult state. Page load times averaged over 8 seconds. The monolithic Express.js backend was deployed on a single EC2 instance that regularly hit 95% CPU during peak hours. Database queries were unoptimized, with some N+1 patterns fetching thousands of rows for a single page render. The team was stuck in a cycle of firefighting production incidents instead of shipping features.
The 876% performance improvement did not come from a single clever optimization. It came from a systematic architectural overhaul that touched every layer of the stack.
Identifying the Bottlenecks
Before writing a single line of new code, we spent two weeks instrumenting the existing application. We added APM tracing to every database query, API call, and rendering step. The data painted a clear picture: 62% of response time was spent on redundant database queries, 21% on synchronous third-party API calls that could be parallelized or cached, and 12% on server-side rendering of data that rarely changed.
The most impactful finding was that the same expensive database aggregation was being computed on every request. A dashboard page that showed project metrics was running a query that joined five tables and aggregated data across months of records. This single query took 3.2 seconds and ran on every page load for every user.
// Before: Computed on every request (3,200ms)
const metrics = await db.query(`
SELECT p.id, p.name,
COUNT(DISTINCT t.id) as task_count,
AVG(t.completion_time) as avg_completion,
SUM(CASE WHEN t.status = 'done' THEN 1 ELSE 0 END) as completed
FROM projects p
JOIN tasks t ON t.project_id = p.id
JOIN activity_log a ON a.task_id = t.id
WHERE p.org_id = $1
AND a.created_at > NOW() - INTERVAL '90 days'
GROUP BY p.id, p.name
`);
// After: Precomputed and cached, invalidated on writes (4ms)
const metrics = await redis.get(`org:${orgId}:dashboard:metrics`);
The Serverless Migration
We decomposed the monolith into focused AWS Lambda functions behind API Gateway. Each function owned a single domain concern: authentication, project management, task operations, and reporting. This was not microservices for its own sake. The decomposition followed clear bounded contexts that the existing codebase had already informally established through its module structure.
The critical architectural change was moving from synchronous request processing to an event-driven model. When a user completed a task, instead of updating the database, recalculating metrics, sending notifications, and updating search indexes all in the same request, we published an event to EventBridge. Downstream consumers handled each concern independently. The user-facing response went from 1.8 seconds to 140 milliseconds.
Results and Takeaways
After three months of incremental migration, the numbers told the story. Average API response time dropped from 4.2 seconds to 430 milliseconds, an improvement of 876%. The P99 went from 12 seconds to 1.8 seconds. Infrastructure costs actually decreased despite handling 3x more traffic because Lambda functions only ran when needed, unlike the always-on EC2 instances.
The lesson I took from Ironforge is that performance is rarely about micro-optimizations. It is about architectural decisions: where you compute, when you compute, and what you choose not to compute at all. Caching the right things, parallelizing independent operations, and moving work out of the critical request path will deliver order-of-magnitude improvements that no amount of code-level tuning can match.