DEV Community

Amaresh Adak
Amaresh Adak

Posted on

I Cut My API Response Time from 1.9s to 200ms - Here's How

Our API was slow. Like, really slow. The kind of slow where you click something and then wonder if you actually clicked it.

For months, I told myself "it's fine, it works." Then I looked at the analytics and saw 28% of users were rage-refreshing during page loads. Oof. 😬

So I spent a week actually fixing it. Went from 1.9 seconds to 200ms. 89% improvement. No rewrites, no new servers, just fixing dumb things I should've fixed months ago.

Here's the whole story with all the code.


The "Oh Crap" Moment

Before I did anything, I needed to know where the time was actually going. I assumed it was the database (it wasn't). I assumed our code was the problem (it wasn't really).

Added some quick logging:

const perfLog = {};

async function trackPerformance(label, fn) {
  const start = Date.now();
  const result = await fn();
  perfLog[label] = Date.now() - start;
  console.log(`${label}: ${Date.now() - start}ms`);
  return result;
}

// Then wrapped everything
const user = await trackPerformance('fetchUser', () => getUserData(id));
const posts = await trackPerformance('fetchPosts', () => getPosts(userId));
Enter fullscreen mode Exit fullscreen mode

After 24 hours of logging, here's what I found:

External API calls:  1,340ms (71%) ← 😱
Database queries:      280ms (15%)
Data processing:       180ms (9%)
Everything else:       100ms (5%)
Enter fullscreen mode Exit fullscreen mode

I'd wasted two whole days optimizing database queries when 71% of our time was waiting on external APIs. Classic developer move: optimizing the wrong thing.

Lesson learned: Always measure. Your gut is probably wrong.


Fix #1: Parallel Everything (The 20-Minute Win)

Here's what I was doing like an absolute noob:

// Making API calls like it's 1999
const userProfile = await externalAPI.getProfile(userId);      // 240ms
const userPosts = await externalAPI.getPosts(userId);          // 290ms
const analytics = await externalAPI.getAnalytics(userId);      // 380ms
const settings = await externalAPI.getSettings(userId);        // 210ms
const notifications = await externalAPI.getNotifications(userId); // 220ms

// Total time: 1,340ms of just... waiting
Enter fullscreen mode Exit fullscreen mode

These calls were 100% independent. None of them needed data from another. Why was I making each one wait?!

Fixed it:

// Run 'em all at once
const [userProfile, userPosts, analytics, settings, notifications] = 
  await Promise.all([
    externalAPI.getProfile(userId),
    externalAPI.getPosts(userId),
    externalAPI.getAnalytics(userId),
    externalAPI.getSettings(userId),
    externalAPI.getNotifications(userId)
  ]);

// Total time: 380ms (just the slowest one)
Enter fullscreen mode Exit fullscreen mode

Result: 1,340ms β†’ 380ms

That's it. That's the fix. Took me 20 minutes. Gave me a 50% improvement in overall response time. Sometimes the biggest wins are the simplest.


πŸ—„οΈ Fix #2: Cache All The Things

With APIs running in parallel, I started looking at what we were calling them for.

User settings? Check.

Analytics summaries? Check.

Recommendation lists? Check.

Then it hit me: this stuff barely changes. User settings might change once a week. Analytics update hourly. Recommendations refresh daily.

Yet here I was, fetching this data fresh on every. single. request. Like we're serving stock prices or something. 🀦

Time for Redis:

const CACHE_TTL = {
  userProfile: 300,      // 5 min - changes occasionally
  userSettings: 3600,    // 1 hour - rarely changes
  analytics: 300,        // 5 min - needs to be freshish
  posts: 180,           // 3 min - fairly dynamic
  recommendations: 86400 // 24 hours - updates once daily
};

async function getCached(key, fetchFn, ttl) {
  // Check cache first
  const cached = await redis.get(key);
  if (cached) {
    console.log(`πŸ’š Cache HIT for ${key}`);
    return JSON.parse(cached);
  }

  // Cache miss - fetch and store
  console.log(`πŸ’” Cache MISS for ${key}`);
  const data = await fetchFn();
  await redis.setex(key, ttl, JSON.stringify(data));
  return data;
}

// Usage
const settings = await getCached(
  `settings:${userId}`,
  () => externalAPI.getSettings(userId),
  CACHE_TTL.userSettings
);
Enter fullscreen mode Exit fullscreen mode

After a few hours, cache hit rate: 68%.

That means we went from 12,000+ external API calls per day to about 3,800. Not only faster, but also:

  • Way cheaper (fewer API calls)
  • More reliable (less dependent on external services)
  • Scales better (Redis is fast)

Result: 900ms β†’ 340ms average response time


Fix #3: The N+1 Query Nightmare

Even though DB wasn't the main problem, I found something that made me cringe:

// First, get all posts (1 query)
const posts = await db.query(
  'SELECT * FROM posts WHERE user_id = ?', 
  [userId]
);

// Then loop through and get comments for each post (N queries)
for (const post of posts) {
  post.comments = await db.query(
    'SELECT * FROM comments WHERE post_id = ?',
    [post.id]
  );
}

// User has 30 posts? That's 1 + 30 = 31 database queries!
Enter fullscreen mode Exit fullscreen mode

Classic N+1 query problem. Every backend dev's enemy.

Fixed with a single query:

const postsWithComments = await db.query(`
  SELECT 
    p.id, 
    p.title, 
    p.content, 
    p.created_at,
    JSON_ARRAYAGG(
      JSON_OBJECT(
        'id', c.id,
        'content', c.content,
        'author', c.author
      )
    ) as comments
  FROM posts p
  LEFT JOIN comments c ON p.id = c.post_id
  WHERE p.user_id = ?
  GROUP BY p.id
`, [userId]);

// ONE query, no matter how many posts
Enter fullscreen mode Exit fullscreen mode

Result: 280ms β†’ 120ms database time

Not huge by itself, but way better under load. 50 concurrent users means 50 queries instead of 1,550.


Fix #4: Stop Sending So Much Garbage

I exported one of our API responses and... 890 KB. For a dashboard. πŸ˜…

Scrolled through the JSON:

  • User object with 30+ fields (frontend used 5)
  • Every post with full content (frontend showed previews)
  • Internal IDs that shouldn't leave the server
  • Debug fields in production
  • Three different timestamp formats (???)

Here's what I did:

// Before: send EVERYTHING
return {
  user: fullUserObject,      // All 30 fields
  posts: allPostsWithAllData, // Everything!
  settings: completeSettings, // Kitchen sink!
  debug: debugInfo           // Why is this here?!
};

// After: send what's needed
return {
  user: {
    id: user.id,
    name: user.name,
    avatarUrl: user.avatarUrl
  },
  posts: posts.map(p => ({
    id: p.id,
    title: p.title,
    preview: p.content.substring(0, 150) + '...',
    date: p.created_at
  })),
  settings: {
    theme: settings.theme,
    language: settings.language
  }
};
Enter fullscreen mode Exit fullscreen mode

Also added gzip (literally one line in Express):

const compression = require('compression');
app.use(compression());
Enter fullscreen mode Exit fullscreen mode

Payload size: 890KB β†’ 125KB (86% smaller!)

Especially important for mobile users and people with slower connections.


Fix #5: Connection Pooling (The Boring One That Matters)

We were doing this:

async function handleRequest(req, res) {
  // Create new connection every time
  const connection = await mysql.createConnection(dbConfig);
  const data = await connection.query(sql);
  await connection.end(); // Close it
  return data;
}
Enter fullscreen mode Exit fullscreen mode

Every. Single. Request.

That's 15-20ms just for the TCP handshake + SSL + auth. Wasteful.

Changed to connection pool:

// Create pool once at startup
const pool = mysql.createPool({
  connectionLimit: 10,
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: 'myapp',
  waitForConnections: true,
  queueLimit: 0
});

// Reuse connections
async function handleRequest(req, res) {
  const data = await pool.query(sql);
  return data;
}
Enter fullscreen mode Exit fullscreen mode

Saved 15-20ms per request. Not huge, but it adds up. Plus way more stable under load.


The Final Numbers

Before:

⏱️  Response time: 1,900ms
πŸ”„  Users refreshing: 28%
πŸ“ž  API calls/day: 12,000+
πŸ—„οΈ  DB queries/request: 30-40
πŸ“¦  Response size: 890KB
Enter fullscreen mode Exit fullscreen mode

After:

⏱️  Response time: 200ms (89% faster!) ⚑
πŸ”„  Users refreshing: 4% (86% drop!)
πŸ“ž  API calls/day: ~3,800 (68% reduction!)
πŸ—„οΈ  DB queries/request: 3-5 (88% fewer!)
πŸ“¦  Response size: 125KB (86% smaller!)
Enter fullscreen mode Exit fullscreen mode

What changed:

  • Session completion: +23%
  • Time on site: +18%
  • "Slow loading" support tickets: basically gone

πŸ› οΈ Tools I Used

Here's my stack for this (but principles work everywhere):

Backend:

Monitoring:

  • New Relic (free tier is solid)
  • Custom performance logging
  • Chrome DevTools Network tab

Testing:


πŸ’‘ Key Takeaways

If I had to do this again, here's what matters:

1. Measure First, Optimize Later

I wasted two days on the wrong thing. Add logging. Look at data. Fix what's actually slow.

2. Parallel > Sequential

If operations are independent, run them in parallel. Free performance.

3. Cache Intelligently

Most data doesn't need to be real-time. Figure out appropriate TTLs.

4. Watch for N+1 Queries

Loop with a query inside = usually bad. Consolidate when possible.

5. Audit Your Payloads

You're probably sending way more than needed. Trim it down.

6. Use Connection Pools

Creating connections is expensive. Reuse them.


πŸ€” Discussion

I'm curious about your experiences:

What's been your biggest API performance win?

Any optimization techniques I missed?

Tools you'd recommend for performance monitoring?

Drop your thoughts below! Always learning from this community. πŸ‘‡


πŸ”— Useful Resources


πŸ“ What's Next?

Thinking about writing follow-ups on:

  • Advanced caching strategies
  • Database indexing deep dive
  • Load testing best practices
  • Monitoring setup guide

Let me know what you'd find most useful!

If this helped, drop a ❀️ and follow for more backend optimization content!


*Have questions about any of the code? Drop them in the comments and I'll help out! *

Top comments (0)