<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Code in Context]]></title><description><![CDATA[Code in Context]]></description><link>https://codeincontextbyag.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 12 Nov 2026 11:50:16 GMT</lastBuildDate><atom:link href="https://codeincontextbyag.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The API Wasn't Slow: Finding a 2-Minute Delay in the Delivery Path]]></title><description><![CDATA[The API requests took 2+ minutes. How Nginx timing logs and testing from multiple places revealed the delivery path bottleneck.]]></description><link>https://codeincontextbyag.hashnode.dev/the-api-wasn-t-slow-finding-a-2-minute-delay-in-the-delivery-path</link><guid isPermaLink="true">https://codeincontextbyag.hashnode.dev/the-api-wasn-t-slow-finding-a-2-minute-delay-in-the-delivery-path</guid><category><![CDATA[nginx]]></category><category><![CDATA[debugging]]></category><category><![CDATA[backend]]></category><category><![CDATA[performance]]></category><category><![CDATA[Devops]]></category><category><![CDATA[api]]></category><category><![CDATA[vpn]]></category><dc:creator><![CDATA[Akshita Gupta]]></dc:creator><pubDate>Sun, 08 Nov 2026 18:50:10 GMT</pubDate><content:encoded><![CDATA[<p>A load test came back with numbers nobody wants to see: responses taking <strong>over two minutes</strong>, and about <strong>one in three completed requests failing</strong>.</p>
<p>The obvious conclusion was that the API was slow. It wasn't.</p>
<p>This post walks through how we narrowed it down, step by step, and the measurement habit that would have saved us time from the start.</p>
<blockquote>
<p><strong>TL;DR:</strong> The database and the API were fast. The time was being lost <em>after</em> the API had its response ready, somewhere between the server and the client. Always compare <strong>backend time</strong> with <strong>total time</strong>, and test from more than one place.</p>
</blockquote>
<hr />
<h2>The setup</h2>
<p>The load test ran from outside the server, over a VPN. Every request travelled the same route:</p>
<pre><code class="language-text">Load test client → VPN → Nginx (reverse proxy) → API → Database
</code></pre>
<p>And the response came back the same way, in reverse.</p>
<p>That gives us five suspects: the database, the API, Nginx, the VPN and network, and the load test client itself.</p>
<hr />
<h2>Step 1: Check the database</h2>
<p>The first suspect in any slow API is usually the database, so that's where we started.</p>
<p>Queries were completing in <strong>milliseconds</strong>. Whatever was taking two minutes, it wasn't the database.</p>
<hr />
<h2>Step 2: Run the same test from the server</h2>
<p>Next, I replayed the same workload <strong>from the API server itself</strong>, still going through Nginx and the API, so only the outside part of the journey was removed.</p>
<table>
<thead>
<tr>
<th>Where the test ran</th>
<th>Failures</th>
<th>Typical response time</th>
</tr>
</thead>
<tbody><tr>
<td>From outside, over the VPN</td>
<td>About 1 in 3</td>
<td>Over 2 minutes</td>
</tr>
<tr>
<td>From the server itself</td>
<td>Zero</td>
<td>Under 100 ms</td>
</tr>
</tbody></table>
<p>Same API, same proxy, same database. The only big difference was <strong>where the requests came from</strong>.</p>
<p>That one comparison narrowed things down enormously. If the API or database were the problem, the test from the server would have been slow too.</p>
<hr />
<h2>Step 3: Read the Nginx logs</h2>
<p>Nginx can log two different timings for every request, and comparing them is the key to this whole story:</p>
<ul>
<li><code>$upstream_response_time</code> is how long the backend, our API, took to send its response to Nginx.</li>
<li><code>$request_time</code> is the total time Nginx spent on the request, from the first bytes it read from the client until it finished sending the response back. That includes reading the request, any queueing, and delivering the response.</li>
</ul>
<p>For one slow request, the logs showed:</p>
<table>
<thead>
<tr>
<th>Timing</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>API (upstream) response time</td>
<td>Under 300 ms</td>
</tr>
<tr>
<td>Total request time</td>
<td>Almost 80 seconds</td>
</tr>
</tbody></table>
<p><strong>The API had its answer ready in under a third of a second. Getting that answer back to the client took almost 80 seconds.</strong></p>
<p>This can happen because Nginx buffers responses by default. The API hands the full response to Nginx quickly and moves on, and Nginx then sends it on to the client at whatever speed the connection allows. If the path to the client is slow, the total time grows while the backend time stays small.</p>
<blockquote>
<p>Note: if response buffering is turned off, for example for streaming responses, a slow client can hold the backend connection open too. In that case the upstream time grows as well, which is a useful clue in itself.</p>
</blockquote>
<blockquote>
<p><strong>Caution:</strong> the gap between the two numbers isn't always response delivery. It can also be a slow request upload or queueing in Nginx. Log <code>request_length</code> and <code>body_bytes_sent</code> to tell them apart.</p>
</blockquote>
<hr />
<h2>So where did the time go?</h2>
<p>The evidence pointed to the <strong>delivery path</strong>, the part of the journey between the server and the client:</p>
<ul>
<li>The database was fast.</li>
<li>The API was fast.</li>
<li>The test from the server, through Nginx, was fast.</li>
<li>Only requests travelling from outside were slow.</li>
</ul>
<p>At this point, the remaining suspects are all on that path:</p>
<ul>
<li><strong>VPN bandwidth.</strong> Every concurrent response shares one tunnel. If a load test pushes more data than the tunnel can carry, responses queue up, waits stretch into minutes, and the longest ones time out, which shows up as failures.</li>
<li><strong>Packet size problems (MTU).</strong> VPNs add extra headers to every packet. If full-size packets no longer fit and the network can't signal that properly, large responses stall while small ones look fine.</li>
<li><strong>The wider network.</strong> Packet loss, a distant gateway, or traffic inspection along the route.</li>
<li><strong>The load test client itself.</strong> A load generator that's short on CPU, memory or bandwidth can't read responses fast enough, which makes the server look slow.</li>
</ul>
<hr />
<h2>How to check this yourself</h2>
<h3>1. Split a single request's time with curl</h3>
<p>Run this from where your users or test client are:</p>
<pre><code class="language-bash">curl -o /dev/null -s -w "first byte: %{time_starttransfer}s | total: %{time_total}s | size: %{size_download} bytes | speed: %{speed_download} bytes/s\n" https://your-api-url
</code></pre>
<p>A <strong>fast first byte with a slow total</strong> means the server responded quickly and the transfer is the slow part.</p>
<h3>2. Run the same test from more than one place</h3>
<p>Compare results from the server itself, from inside the same network, and from where your real users are. The point where things get slow is where to look.</p>
<h3>3. Compare data needed with capacity available</h3>
<p>Work out how much data your test moves per second: response size × requests ÷ test duration. Then measure what the path can actually carry, for example with <code>iperf3</code> across the VPN. If the test needs more than the path can deliver, you've found your bottleneck.</p>
<h3>4. Watch the load generator</h3>
<p>Check CPU, memory and network usage on the machine running the test while it runs. If it's maxed out, your results describe the test machine, not your API.</p>
<hr />
<h2>Lessons</h2>
<ol>
<li><strong>"The API is slow" is a guess, not a diagnosis.</strong> Measure before you optimize.</li>
<li><strong>Backend time and total time are different numbers.</strong> If they're far apart, the problem is outside your code.</li>
<li><strong>Test from multiple places.</strong> One comparison, from the server versus from outside, ruled out most of the stack in minutes.</li>
<li><strong>Load tests test the whole path, including the tester.</strong> Make sure the client, the network and any VPN can handle the load you're generating.</li>
</ol>
<p>A healthy API doesn't always mean a healthy application. Your users don't experience your API on its own. They experience the whole journey.</p>
<p>Because code only makes sense in context.</p>
<hr />
<p><strong>Update (Nov 9):</strong> @lycore pointed out that <code>request_time</code> minus <code>upstream_response_time</code> can also include request upload and queueing, not just response delivery. Fixed the wording above and added <code>request_length</code> and <code>body_bytes_sent</code> to the log format.</p>
<hr />
<p><em>Have you ever chased a "slow API" that turned out not to be slow at all? I'd love to hear your story in the comments.</em></p>
<p><em>If you prefer short videos, this story is also a reel on Instagram at</em> <a href="https://www.instagram.com/codeincontext.ag/"><em>@codeincontext.ag</em></a><em>.</em></p>
]]></content:encoded></item></channel></rss>