ZUKATEK JOURNAL

Server optimization: measure first, change second

An internal response around 3 ms does not explain a user's full wait. Separate processing, network time and asset loading before upgrading a server.

“How do we make the website faster?” is often answered before anyone measures it. Teams add CPU or RAM, then discover that the page still feels slow.

This article follows the measurements recorded in our project draft, explains what they suggest, and identifies what they cannot establish.

The system described

The original draft dates these observations to 18 October 2026. The underlying logs are not available in this folder for independent verification. These are observations from one configuration, not a representative benchmark; repeat them under realistic load.

  • A VPS with four CPU cores and 2.9 GB of RAM in Vietnam.

  • A Node server-rendered public site, static admin interface, Go API and PostgreSQL 16.

  • Another company's application sharing the same server.

Step 1: measure internally

A slow page can combine application work, network time, CDN behavior and large assets. Start by separating them.

A loopback request removes the external network from the measurement. The draft records:

Internal SSR:          3.1 ms   3.3 ms   2.9 ms
Internal /members API: 5.1 ms
Internal /menus API:   4.3 ms
Internal /settings:    3.7 ms
Load average:         0.06
Free RAM:             1.9 GB

These samples show no obvious server bottleneck at that moment. A load average of 0.06 does not mean the CPU is idle 99% of the time. Check CPU, I/O, memory, concurrency and p95/p99 latency under load before drawing a capacity conclusion.

Even a short internal measurement is valuable because it tells you where to investigate next.

Step 2: measure from the user's location

From a user machine in Vietnam, direct connection:
  TLS handshake: 0.06–0.08 s
  TTFB:          0.08–0.10 s
  Total:         0.10–0.13 s

Do not subtract an internal response time from an external total and call the remainder “network delay.” Use the same URL and content. Separate DNS, TCP, TLS, TTFB and body download; repeat measurements while controlling cache state and connection reuse. Also record whether a timing is an individual phase or a cumulative timestamp.

Step 3: weigh the page's assets

The draft's largest opportunities were outside application processing:

Asset

Original

Compressed or converted

entry-client.js

482 KB

152 KB

esm.js

318 KB

Not recorded

CSS

135 KB

30 KB

One profile image

697 KB

516 KB, WebP

logo.png

140 KB

Not recorded

favicon.png

49 KB

Not recorded

18 font files

290 KB

Not recorded

That profile image is larger than the compressed entry-client.js file. The table does not establish the total size of all compressed JavaScript. Even so, 516 KB deserves attention for an image displayed about 320 pixels wide.

The draft also reports that adding ?w=320 still returned the original image. A resizing parameter is useful only if the server actually implements it.

A reasonable investigation order is images, compression, static asset caching, JavaScript, then hardware when CPU, memory or I/O measurements justify it. Let your actual page data change that order when necessary.

Step 4: verify compression on the response

An nginx configuration containing gzip on; is not proof that every intended asset is compressed. The gzip module supports text/html; CSS and JavaScript need appropriate MIME types in gzip_types.

Ask for a compressed response and inspect the transferred size:

curl -s -o /dev/null --compressed -w '%{size_download}' https://your-domain/

Check Content-Encoding too, and compare the same response without compression. A smaller body alone does not identify the algorithm. The draft records roughly 91 KB of HTML and about 21 KB transferred, but no original header is available here to verify its earlier attribution to zstd.

Step 5: avoid a convenient but misleading test

The first CDN test in the draft ran from the origin server itself. It recorded 348–838 ms, but followed origin → internet → CDN → origin: a route different from a real user's request.

A later client-side test recorded 0.31–0.49 seconds. However, the direct and CDN routes then tested different applications. Their difference cannot quantify the CDN's overhead.

Measure where users are, and compare like with like.

Where a request goes — and what each leg measuresBrowser → networkMeasured from a real user's machine100–130 msnginxForwardingunder 1 msApplication (SSR / API)Measured on loopback3 msPostgreSQLAlready inside the number aboveunder 2 msStatic assetsImages, JavaScript, fontsnot measured yetThe server answers in 3 ms on loopback; the user waits 100–130 ms. Those aretwo measurements taken under different conditions — measure the network andthe assets separately before attributing the wait to one cause.

Step 6: use a control measurement

The project account describes certificate issuance failing with a connection reset. Packet capture reportedly showed no incoming SYN on port 80 from an overseas network, while a domestic connection reached the server normally.

That comparison helps locate a possible routing or filtering problem before the server. It does not by itself prove the hosting provider intentionally blocked the port.

The useful lesson is broader: a measurement that finds nothing needs a control that finds something. Otherwise you cannot distinguish absence of traffic from a faulty capture or test setup.

A practical sequence

  1. Measure loopback responses.

  2. Measure from real user locations.

  3. Sort assets by transferred size.

  4. Check compression headers and bytes.

  5. Inspect edge cache status.

  6. Evaluate hardware when the evidence points there.


Figures are retained from the Zukatek draft for October 2026 and have not been independently reproduced in this review. Keep the method, and save the logs of your own measurements.

Reference: nginx gzip module.

BUILD SIMPLE. GROW BEYOND.

Thanks for reading, and for travelling with Zukatek.

← Back to the blog