Skip to main content

Recorded on 2026-09-01 with the JWT sessions of that day. The session paths and the token wording below describe that day, not the current design. For today's design read the Rails authentication pages and the JodApp Web authentication pages.

Web latency baseline — prod, measured from a laptop

This page holds one benchmark run. It measures how long the Jod web server and the Jod Rails API take to answer.

It exists to answer one question. Should a page fetch its data in a React Router loader() or in a clientLoader()?

  • A loader() runs on the web server. The web server calls the Rails API over a private name inside the VPC. Then it sends finished HTML to the browser. The user waits once.
  • A clientLoader() runs in the browser. The page shell arrives first. Then the browser calls the Rails API itself over the public internet, through Cloudflare. The user waits twice, and the second wait leaves the page half empty.

The two shapes pay different network costs. This run measures both costs so the choice rests on numbers, not on a guess.

This run is a baseline, not the final answer​

A new session middleware is coming to the web server. On every navigation that carries a session cookie, it will call GET /identities/sessions/current on the Rails API. That middleware does not exist yet. It did not run during this benchmark.

So treat these numbers as the "before" picture. Run the same script again after the middleware ships, with the same flags. Then put the two reports side by side.

What was measured, and from where​

ItemValue
Environmentprod
Modeclient (from a laptop over the public internet)
Started (UTC)2026-09-01T14:32:22Z
Finished (UTC)2026-09-01T14:35:22Z
Run time177 seconds
Machine host nameNerdherd-M2-Max.local
Machine kernelDarwin 25.5.0 arm64
curlcurl 8.7.1 (x86_64-apple-darwin25.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.68.1
Network the run came fromhome fibre, jakarta, indihome
Public web hostjodapp.com
Public API hostapi.jodapp.com
Private API hostapi.internal.jodapp.com
Measured requests per measurement20
Warm-up requests per measurement3
Total requests sent253
User-Agent, API requestsjod-latency-benchmark/1.0
User-Agent, page requestsMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 jod-latency-benchmark/1.0
Session cookiesupplied. Its value is not in this report.

The two API endpoints, and why there are two​

This report measures the Rails API twice, with two different endpoints. They answer different questions.

EndpointWhat it doesWhat its number means
/upThe Rails health route. It touches no database.The network floor. The smallest cost the path can have. No page ever calls it, so it is not the cost of a data fetch.
GET /identities/sessions/currentVerifies the JWT from the session cookie, then asks two services whether the user may enter the talent area and the employer area.One real signed-in fetch. This is what a page actually pays, and it is the endpoint the coming middleware calls.

The gap between the two is the Rails work. That split matters. Only one half of it changes when you move a fetch from the server to the browser.

  • The network half changes a lot. The private path stays inside the VPC. The public path crosses the internet and Cloudflare.
  • The Rails half does not change at all. The same query runs either way.

So read the two numbers together. Reading /up on its own leads to the wrong conclusion, in a way that is easy to miss.

  • The gap in milliseconds between the two paths is the same either way. The Rails work is added to both sides, so it cancels out.
  • What changes is how large that gap looks next to the whole wait.
  • On /up alone, the gap is almost the entire number. Next to a real fetch it is one part of a larger total.

That share is what the decision turns on. A gap of 30 ms matters a lot when the whole fetch takes 35 ms. It matters much less when the whole fetch takes 300 ms.

Does this environment serve the session endpoint?​

Environments do not always run the same build. So the script checks before it starts, instead of assuming. It sends one request with no cookie and reads the status code.

AnswerWhat it meansWhat the script does
401The route is there. The cookie is simply missing on a check request.Measures the real fetch.
404This Rails API does not serve the path.Skips the measurement, rather than recording 404 timings as a result.

On this run, /identities/sessions/current answered on prod. So the numbers below include one real signed-in fetch.

Why the GET, and not the session refresh​

The path /identities/sessions/current answers four verbs. Only one of them belongs in a benchmark.

VerbCan this benchmark use it?
GETYes. It changes nothing. It verifies the JWT, then asks two services whether the user may enter the talent area and the employer area.
PATCHNo. It rotates the JWT and sets new cookies. That is a write. It would also end the session being used to run the benchmark, because each call replaces the cookie the last one used.
POSTNo. It signs a user in. It is a write, and it needs a password.
DELETENo. It signs the user out. It is a write.

This script only ever sends GET requests. It never touches the other three.

The GET is the right one for a second reason. The coming middleware calls this exact endpoint on every request that carries a session cookie. So its cost is the cost the middleware will add.

A heavier endpoint to compare against​

The session endpoint is deliberately small. It answers who is signed in and where they may go, and it carries no page data.

For a heavier read, run the script again with --session-endpoint /identities/users/current. That one loads the user with about ten associated tables and serialises the lot. It shows what a fatter fetch costs on the same two paths.

How to read the numbers​

Every number in this report is milliseconds.

TermWhat it means
TTFB (time_starttransfer)The wait until the first byte of the response arrives. This is the wait the user feels.
Whole response (time_total)The wait until the last byte arrives. For a big HTML page this is larger than TTFB.
DNS (time_namelookup)Time spent turning the host name into an IP address.
TCP connect (time_connect)Time spent opening the TCP connection.
TLS handshake (time_appconnect)Time spent on the TLS handshake. It is 0 on plain HTTP.
Ready to send (time_pretransfer)Everything above, finished. The request has not left yet.

Two connection shapes appear below.

ShapeHow it was producedWhat it stands for
New connection each timeOne curl process per request. A new process cannot reuse a connection.The first call after a cold start. It pays DNS, TCP and TLS.
Connection reusedOne curl process, many URLs. curl keeps the connection open.Real life. Node's built-in fetch keeps connections alive by default, and so does a browser. Use this number when you reason about loader() and clientLoader().

In a reused-connection measurement the DNS, TCP and TLS rows read close to 0. That is correct. The connection was already open, so no time was spent opening it.

The p95 uses the nearest-rank method. Sort the samples, then take the sample at position ceil(0.95 times n). With 20 samples that is the 19th.

Warm-up requests are listed under each table. They are never mixed into min, median, mean, p95 or max.

Every page request carries a cb query parameter. Its value is different every time. So no cache can answer the request.

Every measured request sends Accept-Encoding for gzip, the way a browser and Node's fetch both do.

Warnings from this run​

Read these before you trust any number below.

  • Public page /up, the control, new connection each time: the page did not stream. Time to first byte (155.15 ms) is almost the whole response (156.34 ms). The usual cause is that the page defers no data: with no Suspense boundary and no deferred loader value, React Router must resolve every loader before it sends any HTML, so nothing can arrive early. A User-Agent the server treats as a bot has the same effect. Neither is a fault in this script.
  • Public page /up, the control, connection reused: the page did not stream. Time to first byte (337.85 ms) is almost the whole response (343.39 ms). The usual cause is that the page defers no data: with no Suspense boundary and no deferred loader value, React Router must resolve every loader before it sends any HTML, so nothing can arrive early. A User-Agent the server treats as a bot has the same effect. Neither is a fault in this script.
  • Public page /jobs, new connection each time: the page did not stream. Time to first byte (1295.36 ms) is almost the whole response (1302.34 ms). The usual cause is that the page defers no data: with no Suspense boundary and no deferred loader value, React Router must resolve every loader before it sends any HTML, so nothing can arrive early. A User-Agent the server treats as a bot has the same effect. Neither is a fault in this script.
  • Public page /jobs, connection reused: the page did not stream. Time to first byte (608.30 ms) is almost the whole response (610.44 ms). The usual cause is that the page defers no data: with no Suspense boundary and no deferred loader value, React Router must resolve every loader before it sends any HTML, so nothing can arrive early. A User-Agent the server treats as a bot has the same effect. Neither is a fault in this script.

Results​

Network floor, browser to Rails API, public internet, new connection each time​

The smallest cost the public path can have. /up touches no database, so this is network, Cloudflare and TLS, and nothing else.

  • URL: https://api.jodapp.com/up
  • Connection shape: new connection each time
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 23
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
Measureminmedianmeanp95max
TTFB (time_starttransfer)91.81451.46575.811276.261633.59
Whole response (time_total)92.21452.64576.931277.631634.48
DNS (time_namelookup)1.922.222.453.203.27
TCP connect (time_connect)22.05188.69140.83227.84242.60
TLS handshake (time_appconnect)49.13306.14294.81651.72773.90
Ready to send (time_pretransfer)49.19306.36294.98651.92774.32

All values are milliseconds.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
1196.81197.14200
2106.02106.31200
387.7388.03200

Network floor, browser to Rails API, public internet, connection reused​

The same network floor with the connection already open. No page ever calls /up, so this is not the cost of a data fetch.

  • URL: https://api.jodapp.com/up
  • Connection shape: connection reused
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 1
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
Measureminmedianmeanp95max
TTFB (time_starttransfer)377.74381.95399.03437.43460.93
Whole response (time_total)378.97382.72400.12438.56462.04
DNS (time_namelookup)0.020.040.040.050.08
TCP connect (time_connect)0.000.000.000.000.00
TLS handshake (time_appconnect)not usednot usednot usednot usednot used
Ready to send (time_pretransfer)0.100.230.230.370.40

All values are milliseconds.

The TLS row says "not used" because no TLS handshake happened. Either the request went over plain HTTP, or the connection was already open.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
1832.30833.59200
2377.75378.82200
3379.43380.42200

In this measurement request 1 also opened the connection. That is why it is slower than the rest.

One real signed-in fetch, public internet, new connection each time​

What the first real clientLoader() API call costs. It pays DNS, TCP and TLS on top of the Rails work.

  • URL: https://api.jodapp.com/identities/sessions/current
  • Connection shape: new connection each time
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 23
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
  • Sent a session cookie. Its value is not in this report.
Measureminmedianmeanp95max
TTFB (time_starttransfer)99.93422.16523.011055.871808.12
Whole response (time_total)100.33423.48524.101057.251809.61
DNS (time_namelookup)1.932.822.673.333.73
TCP connect (time_connect)21.75156.51129.53214.52264.53
TLS handshake (time_appconnect)49.86326.00281.71597.52648.53
Ready to send (time_pretransfer)49.97326.23281.93597.70648.77

All values are milliseconds.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
11260.971263.35200
21018.491019.57200
3698.83700.35200

One real signed-in fetch, public internet, connection reused​

What every later real clientLoader() API call costs. This is the number the decision rests on.

  • URL: https://api.jodapp.com/identities/sessions/current
  • Connection shape: connection reused
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 1
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
  • Sent a session cookie. Its value is not in this report.
Measureminmedianmeanp95max
TTFB (time_starttransfer)38.3341.7044.7548.3399.07
Whole response (time_total)39.2642.4449.8299.44133.10
DNS (time_namelookup)0.010.030.030.030.04
TCP connect (time_connect)0.000.000.000.000.00
TLS handshake (time_appconnect)not usednot usednot usednot usednot used
Ready to send (time_pretransfer)0.050.200.180.240.34

All values are milliseconds.

The TLS row says "not used" because no TLS handshake happened. Either the request went over plain HTTP, or the connection was already open.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
1117.33120.41200
240.1340.38200
339.9240.89200

In this measurement request 1 also opened the connection. That is why it is slower than the rest.

Public page /up, the control, new connection each time​

The smallest cost any page request can have from this network. It runs no loader and no session code.

  • URL: https://jodapp.com/up
  • Connection shape: new connection each time
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 23
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
Measureminmedianmeanp95max
TTFB (time_starttransfer)94.51155.15434.781006.171855.24
Whole response (time_total)94.86156.34436.151007.511858.45
DNS (time_namelookup)1.832.252.453.203.28
TCP connect (time_connect)21.5361.37103.72214.69285.64
TLS handshake (time_appconnect)46.4890.47202.21414.44840.23
Ready to send (time_pretransfer)46.5590.57202.37414.65840.47

All values are milliseconds.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
12265.962268.65200
2186.12187.61200
31155.501155.89200

Public page /up, the control, connection reused​

The same control without DNS, TCP and TLS.

  • URL: https://jodapp.com/up
  • Connection shape: connection reused
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 1
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
Measureminmedianmeanp95max
TTFB (time_starttransfer)331.70337.85344.77366.08368.41
Whole response (time_total)337.18343.39349.10366.33370.01
DNS (time_namelookup)0.010.030.040.070.07
TCP connect (time_connect)0.000.000.000.000.00
TLS handshake (time_appconnect)not usednot usednot usednot usednot used
Ready to send (time_pretransfer)0.060.180.190.340.35

All values are milliseconds.

The TLS row says "not used" because no TLS handshake happened. Either the request went over plain HTTP, or the connection was already open.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
1797.94799.41200
2340.55346.40200
3337.32339.22200

In this measurement request 1 also opened the connection. That is why it is slower than the rest.

Public page /privacy, new connection each time​

What a first-time visitor waits for a light page.

  • URL: https://jodapp.com/privacy
  • Connection shape: new connection each time
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 23
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
Measureminmedianmeanp95max
TTFB (time_starttransfer)623.771319.971360.651998.154463.99
Whole response (time_total)624.651499.411470.952147.905062.70
DNS (time_namelookup)2.022.562.603.373.42
TCP connect (time_connect)21.95179.39128.73208.79213.09
TLS handshake (time_appconnect)49.22386.62306.97436.61909.92
Ready to send (time_pretransfer)49.31386.85307.20436.75910.10

All values are milliseconds.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
12107.862469.79200
2612.05613.46200
3707.57708.95200

Public page /privacy, connection reused​

What a returning visitor waits for a light page.

  • URL: https://jodapp.com/privacy
  • Connection shape: connection reused
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 1
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
Measureminmedianmeanp95max
TTFB (time_starttransfer)897.74922.70937.331017.591032.97
Whole response (time_total)1071.381085.851099.721174.811197.74
DNS (time_namelookup)0.020.050.050.050.07
TCP connect (time_connect)0.000.000.000.000.00
TLS handshake (time_appconnect)not usednot usednot usednot usednot used
Ready to send (time_pretransfer)0.110.260.250.290.30

All values are milliseconds.

The TLS row says "not used" because no TLS handshake happened. Either the request went over plain HTTP, or the connection was already open.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
11773.921845.99200
2906.831080.16200
3922.241091.00200

In this measurement request 1 also opened the connection. That is why it is slower than the rest.

Public page /jobs, new connection each time​

A public list page with real loader() work, measured from outside.

  • URL: https://jodapp.com/jobs
  • Connection shape: new connection each time
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 23
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
Measureminmedianmeanp95max
TTFB (time_starttransfer)655.191295.361254.971965.632073.42
Whole response (time_total)657.111302.341266.412055.792074.68
DNS (time_namelookup)1.873.042.803.473.82
TCP connect (time_connect)21.69176.28136.07213.06233.30
TLS handshake (time_appconnect)49.02398.03351.73617.801325.48
Ready to send (time_pretransfer)49.10398.25351.91618.011325.75

All values are milliseconds.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
1934.16935.56200
21128.401130.56200
3870.59872.11200

Public page /jobs, connection reused​

The same page without DNS, TCP and TLS.

  • URL: https://jodapp.com/jobs
  • Connection shape: connection reused
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 1
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
Measureminmedianmeanp95max
TTFB (time_starttransfer)580.52608.30634.75730.88764.30
Whole response (time_total)583.94610.44636.52732.33767.51
DNS (time_namelookup)0.010.040.040.060.14
TCP connect (time_connect)0.000.000.000.000.00
TLS handshake (time_appconnect)not usednot usednot usednot usednot used
Ready to send (time_pretransfer)0.070.220.190.270.34

All values are milliseconds.

The TLS row says "not used" because no TLS handshake happened. Either the request went over plain HTTP, or the connection was already open.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
1646.10647.16200
2588.95591.75200
3687.31688.96200

In this measurement request 1 also opened the connection. That is why it is slower than the rest.

Public page /employers/dashboard, signed in, connection reused​

The real employer journey, measured from outside.

  • URL: https://jodapp.com/employers/dashboard
  • Connection shape: connection reused
  • Requests: 3 warm-up, 20 measured
  • Connections opened: 1
  • Status codes: 200 x23
  • HTTP version: HTTP/2 x23
  • cf-cache-status: DYNAMIC x23
  • Sent a session cookie. Its value is not in this report.
Measureminmedianmeanp95max
TTFB (time_starttransfer)375.32422.11555.071063.971477.82
Whole response (time_total)379.49432.64559.591068.141489.87
DNS (time_namelookup)0.030.040.040.050.06
TCP connect (time_connect)0.000.000.000.000.00
TLS handshake (time_appconnect)not usednot usednot usednot usednot used
Ready to send (time_pretransfer)0.130.260.240.320.33

All values are milliseconds.

The TLS row says "not used" because no TLS handshake happened. Either the request went over plain HTTP, or the connection was already open.

Warm-up requests. They are shown here and left out of the table above.

RequestTTFBWhole responseStatus
1816.49820.05200
2386.20387.29200
3370.94374.91200

In this measurement request 1 also opened the connection. That is why it is slower than the rest.

What these numbers mean for loader() and clientLoader()​

QuestionAnswer from this run
What does one real API call from a clientLoader() cost?41.70 ms, median, on a connection that is already open
How much of that is network, Cloudflare and TLS?381.95 ms, median, the /up floor
How much of that is Rails work?-340.25 ms
What does that real call cost when the connection is new?422.16 ms, median, including DNS, TCP and TLS
What does opening the connection add?380.46 ms
How much of the network floor is DNS?2.22 ms, median
How much of it is TCP?188.69 ms, median
How much of it is TLS?306.14 ms, median
What does the page shell cost on its own?343.39 ms, median, for /up
What does a light public page cost?1085.85 ms, median, for /privacy
What does a public list page cost?610.44 ms, median, for /jobs
What does the signed-in employer dashboard cost?432.64 ms, median

Read it like this.

  • One real API call from the browser costs about 41.70 ms once the connection is open. A clientLoader() pays this on top of the page shell, and the user sees an empty area while it runs.
  • The real call was no slower than /up. That is surprising for a call that reads the database. Check the status codes and the raw samples before you trust it.
  • The network on its own costs about 381.95 ms. That is /up, which touches no database. No page ever calls it. It is the smallest cost this path can have, not a result.
  • The very first call to the API host costs about 380.46 ms more, because it pays DNS, TCP and TLS. A clientLoader() on a fresh visit pays this. A loader() never does, because the web server holds its connection open.
  • A clientLoader() page has two waits. The shell arrives after about 343.39 ms. The data arrives about 41.70 ms after that. Total: about 385.09 ms before the page is complete.
  • A loader() page has one wait. /jobs takes about 610.44 ms, and it arrives complete.
  • On a first visit, /jobs costs about 691.90 ms more, because the browser has to open a connection to the web host first.

The honest comparison needs both reports​

This run measured the public path only. It cannot tell you what the web server pays to reach the Rails API, because that path lives inside the VPC.

Run the server-side mode on the web instance, then read the two reports together: --mode server --env prod

Compare the same endpoint on both paths. Not /up against /up.

ShapeWhat the user waits forWhich number to use
loader()One wait. The page shell plus one private API call.The real signed-in fetch, private name, connection reused. It is in the server-side report.
clientLoader()Two waits. The page shell first, then one public API call.The real signed-in fetch, public internet, connection reused. It is in the table above.

Both rows name the same endpoint. The only difference is the path it travels. That is the comparison the decision needs.

One more point that no benchmark can measure. A loader() sends finished HTML. A clientLoader() sends a shell, so search engines and link previews see an empty page unless something else fills it in.

The exact commands this run used​

Each line below sends one request. The script sent the same request many times.

For a reused-connection measurement, the script passed every URL to one curl call. curl then kept the connection open between them.

Network floor, browser to Rails API, public internet, new connection each time​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'jod-latency-benchmark/1.0' -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://api.jodapp.com/up'

Network floor, browser to Rails API, public internet, connection reused​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'jod-latency-benchmark/1.0' -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://api.jodapp.com/up'

One real signed-in fetch, public internet, new connection each time​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'jod-latency-benchmark/1.0' -K cookie.conf -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://api.jodapp.com/identities/sessions/current'

About cookie.conf:

  • It holds one line: header = "Cookie: ...".
  • The quotes are required. In a curl config file an unquoted value stops at the first space, and a cookie header always has spaces in it.
  • The script writes it into a private temporary folder, with file mode 600.
  • The script deletes it when it exits.
  • curl reads the cookie from that file, never from the command line. So the cookie never shows up in ps output.

One real signed-in fetch, public internet, connection reused​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'jod-latency-benchmark/1.0' -K cookie.conf -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://api.jodapp.com/identities/sessions/current'

About cookie.conf:

  • It holds one line: header = "Cookie: ...".
  • The quotes are required. In a curl config file an unquoted value stops at the first space, and a cookie header always has spaces in it.
  • The script writes it into a private temporary folder, with file mode 600.
  • The script deletes it when it exits.
  • curl reads the cookie from that file, never from the command line. So the cookie never shows up in ps output.

Public page /up, the control, new connection each time​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 jod-latency-benchmark/1.0' -H 'Accept: text/html,application/xhtml+xml' -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://jodapp.com/up'

Public page /up, the control, connection reused​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 jod-latency-benchmark/1.0' -H 'Accept: text/html,application/xhtml+xml' -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://jodapp.com/up'

Public page /privacy, new connection each time​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 jod-latency-benchmark/1.0' -H 'Accept: text/html,application/xhtml+xml' -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://jodapp.com/privacy'

Public page /privacy, connection reused​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 jod-latency-benchmark/1.0' -H 'Accept: text/html,application/xhtml+xml' -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://jodapp.com/privacy'

Public page /jobs, new connection each time​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 jod-latency-benchmark/1.0' -H 'Accept: text/html,application/xhtml+xml' -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://jodapp.com/jobs'

Public page /jobs, connection reused​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 jod-latency-benchmark/1.0' -H 'Accept: text/html,application/xhtml+xml' -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://jodapp.com/jobs'

Public page /employers/dashboard, signed in, connection reused​

curl -sS -o /dev/null -D - --compressed --max-time 20 -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 jod-latency-benchmark/1.0' -H 'Accept: text/html,application/xhtml+xml' -K cookie.conf -w 'TIMING %{time_namelookup} %{time_connect} %{time_appconnect} %{time_pretransfer} %{time_starttransfer} %{time_total} %{http_code} %{size_download} %{num_connects} %{http_version}' 'https://jodapp.com/employers/dashboard'

About cookie.conf:

  • It holds one line: header = "Cookie: ...".
  • The quotes are required. In a curl config file an unquoted value stops at the first space, and a cookie header always has spaces in it.
  • The script writes it into a private temporary folder, with file mode 600.
  • The script deletes it when it exits.
  • curl reads the cookie from that file, never from the command line. So the cookie never shows up in ps output.

How to reproduce this run​

./jod-latency-benchmark.sh --mode client --env prod --samples 20 --warmup 3 --network "home fibre, jakarta, indihome"

Raw samples​

Every request this run made, in the order it made them. Times are milliseconds.

Open the raw samples

Network floor, browser to Rails API, public internet, new connection each time​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.86101.18142.57142.64196.81197.142008212
2warmup1.9022.6652.6752.73106.02106.312008212
3warmup2.0622.5250.7650.8387.7388.032008212
4sample1.94212.62263.72263.93331.88332.812008212
5sample2.90211.63437.08437.27827.55829.162008212
6sample1.9822.0556.5056.58108.86109.162008212
7sample2.3723.1951.0951.1691.8192.212008212
8sample1.96227.84437.22437.441276.261277.632008212
9sample2.19135.72282.61282.86517.76519.022008212
10sample2.9122.8152.8252.9196.8398.322008212
11sample2.19131.03329.67329.87565.76566.822008212
12sample2.89242.60773.90774.311633.591634.482008212
13sample2.91201.65408.53408.74800.72801.362008212
14sample2.1523.2149.1349.1999.76100.072008212
15sample1.9224.2954.9254.98223.75225.812008212
16sample2.2522.9255.6355.7195.1695.542008212
17sample2.06199.27410.51410.70889.01892.562008212
18sample3.20212.16267.33267.49320.45320.772008212
19sample2.08214.21651.72651.921054.931056.232008212
20sample3.1889.53116.56116.67160.16161.282008212
21sample2.39178.10408.49408.691069.701070.912008212
22sample3.27213.52345.60345.80385.17386.252008212
23sample2.17208.18443.16443.40967.11968.172008212

Network floor, browser to Rails API, public internet, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.35194.00448.62448.76832.30833.592008212
2warmup0.050.000.000.25377.75378.822008202
3warmup0.050.000.000.25379.43380.422008202
4sample0.080.000.000.37424.14425.652008202
5sample0.030.000.000.17381.11381.992008202
6sample0.030.000.000.15379.77380.662008202
7sample0.040.000.000.40379.53380.502008202
8sample0.040.000.000.29427.02428.072008202
9sample0.050.000.000.31380.97381.902008202
10sample0.050.000.000.24382.79383.452008202
11sample0.030.000.000.22378.84380.422008202
12sample0.040.000.000.20429.80431.022008202
13sample0.050.000.000.26378.87379.812008202
14sample0.040.000.000.20378.42379.482008202
15sample0.040.000.000.24377.74378.972008202
16sample0.040.000.000.25428.17428.842008202
17sample0.030.000.000.13437.43438.562008202
18sample0.040.000.000.24460.93462.042008202
19sample0.050.000.000.26384.98386.122008202
20sample0.030.000.000.15380.00381.902008202
21sample0.040.000.000.19424.95426.462008202
22sample0.030.000.000.16386.44386.932008202
23sample0.020.000.000.10378.60379.702008202

One real signed-in fetch, public internet, new connection each time​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.08214.84745.57745.821260.971263.3520018212
2warmup1.91186.61386.55386.821018.491019.5720018212
3warmup2.93177.34362.05362.28698.83700.3520018212
4sample2.22175.20360.14360.41793.74795.0520018212
5sample3.0825.8653.0553.17114.66116.2420018212
6sample2.75211.10335.31335.55390.35391.4920018212
7sample3.1622.7853.4053.5199.93100.3320018212
8sample2.31200.48497.20497.48895.48896.8520018212
9sample3.7323.8351.5151.62104.78105.1620018212
10sample2.08264.53475.20475.69838.10839.5920018212
11sample3.0823.6049.8649.97100.17101.7420018212
12sample1.9321.7551.9652.04108.62108.9420018212
13sample2.01212.14550.96551.401808.121809.6120018212
14sample3.2097.54223.82224.22422.85424.3220018212
15sample3.2723.4554.0054.11121.37121.7920018212
16sample1.93137.82319.41319.64561.10562.4520018212
17sample2.9323.5650.9151.04100.67101.0320018212
18sample2.05107.20139.57139.73187.02188.0020018212
19sample2.08208.88597.52597.70998.891000.0720018212
20sample3.33210.37332.58332.82550.94552.0620018212
21sample3.00214.52359.33359.48421.48422.6420018212
22sample2.44175.66429.98430.30786.08787.3420018212
23sample2.89210.41648.53648.771055.871057.2520018212

One real signed-in fetch, public internet, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.2342.6570.7170.79117.33120.4120018212
2warmup0.010.000.000.0740.1340.3820018202
3warmup0.010.000.000.0739.9240.8920018202
4sample0.040.000.000.3438.7640.3220018202
5sample0.030.000.000.2140.5441.3420018202
6sample0.030.000.000.2040.2041.0820018202
7sample0.030.000.000.2342.6143.2120018202
8sample0.020.000.000.1538.3339.2620018202
9sample0.030.000.000.2142.2143.0020018202
10sample0.030.000.000.2099.0799.4420018202
11sample0.020.000.000.1545.2846.2220018202
12sample0.030.000.000.2448.3349.1620018202
13sample0.030.000.000.2241.1841.7220018202
14sample0.020.000.000.1039.6240.4020018202
15sample0.030.000.000.2441.0841.9420018202
16sample0.030.000.000.2142.6343.4420018202
17sample0.030.000.000.2040.8841.4720018202
18sample0.020.000.000.1640.1940.5320018202
19sample0.010.000.000.1143.5843.7520018202
20sample0.010.000.000.0545.21133.1020018202
21sample0.020.000.000.1042.9043.2320018202
22sample0.010.000.000.1142.5942.9420018202
23sample0.030.000.000.1439.7740.8320018202

Public page /up, the control, new connection each time​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.22193.06394.21394.432265.962268.65200474212
2warmup2.8123.65124.12124.26186.12187.61200474212
3warmup2.99174.77422.76423.011155.501155.89200474212
4sample2.25285.64840.23840.471855.241858.45200474212
5sample3.28101.46207.62207.81585.51586.50200474212
6sample3.20191.51390.85391.08904.18905.80200474112
7sample3.09178.12362.98363.221006.171007.51200474212
8sample2.9223.5349.1649.26100.27101.69200474212
9sample1.97199.40414.44414.65873.05875.36200474212
10sample3.0123.7454.4654.55111.70112.24200474212
11sample1.92190.96391.46391.85860.98862.93200474212
12sample3.1323.6050.6950.80106.49109.39200474012
13sample2.2622.5047.6847.76129.88131.37200474212
14sample2.06174.14288.12288.39701.03702.11200474112
15sample1.8321.7546.4846.55103.46104.62200474212
16sample2.2222.9750.0850.14125.31126.86200474212
17sample2.9923.2951.2151.2898.2198.63200474212
18sample2.00212.21261.94262.13313.60314.93200474312
19sample2.6922.7753.3053.39110.61111.06200474212
20sample1.9699.01126.48126.60180.41180.90200474212
21sample1.9121.6246.9547.0194.5194.87200474212
22sample1.8921.5352.0052.10129.90131.78200474212
23sample2.46214.69258.09258.30305.04305.97200474212

Public page /up, the control, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.34265.83453.28453.70797.94799.41200474112
2warmup0.050.000.000.22340.55346.40200474202
3warmup0.020.000.000.10337.32339.22200474202
4sample0.050.000.000.18352.93355.26200474202
5sample0.070.000.000.29334.58339.98200474202
6sample0.040.000.000.27331.70337.18200474202
7sample0.020.000.000.14368.41370.01200474102
8sample0.030.000.000.17333.51341.36200474202
9sample0.040.000.000.19338.19344.55200474102
10sample0.030.000.000.15359.75361.11200474202
11sample0.010.000.000.06334.97339.56200474102
12sample0.020.000.000.17339.77351.58200474202
13sample0.060.000.000.33358.78360.20200474202
14sample0.040.000.000.26336.45342.23200474202
15sample0.030.000.000.19332.97338.33200474202
16sample0.070.000.000.34366.08366.33200474202
17sample0.010.000.000.06337.52341.67200474202
18sample0.060.000.000.25335.45341.77200474102
19sample0.050.000.000.35363.05363.42200474102
20sample0.010.000.000.09335.73341.86200474102
21sample0.010.000.000.10341.95346.07200474202
22sample0.030.000.000.18360.92361.61200474102
23sample0.020.000.000.09332.70337.89200474202

Public page /privacy, new connection each time​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup3.27191.84401.74401.982107.862469.792001488712
2warmup1.9922.2948.5048.58612.05613.462001486112
3warmup2.0923.30139.36139.88707.57708.952001486312
4sample3.42177.20436.61436.751670.861707.962001488612
5sample2.1823.7252.4152.51642.50646.712001486312
6sample2.23213.09909.92910.104463.995062.702001488812
7sample3.3793.01248.98249.711033.741098.352001489012
8sample2.02208.79420.65420.911355.171542.962001488612
9sample2.65203.62419.43419.711718.571871.822001481512
10sample2.5623.2950.1450.23684.87687.112001486112
11sample2.1221.95140.94141.16730.70731.682001486412
12sample2.05196.00427.45427.741998.152147.902001488612
13sample3.1723.4549.2249.31704.26708.342001486312
14sample2.32189.96388.59388.861314.911500.252001488812
15sample3.1026.26148.68149.30745.10748.962001486212
16sample2.34191.66416.04416.321349.551530.422001488512
17sample2.7123.6651.8351.91623.77624.652001486312
18sample2.5722.2551.0951.19802.01804.202001486512
19sample2.06174.45357.04357.181619.861635.692001489012
20sample3.22181.58389.78389.941262.881433.302001489012
21sample3.04187.47385.71385.901577.581720.552001488712
22sample2.17193.12387.54387.801589.411716.932001488712
23sample2.77200.00407.31407.561325.031498.562001488512

Public page /privacy, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup1.99192.12391.77392.011773.921845.992001488712
2warmup0.050.000.000.30906.831080.162001488802
3warmup0.040.000.000.20922.241091.002001488502
4sample0.070.000.000.27906.181077.882001488902
5sample0.050.000.000.28908.901086.932001488802
6sample0.040.000.000.23897.741071.382001488602
7sample0.050.000.000.28904.501074.722001488902
8sample0.040.000.000.201017.591086.522001488802
9sample0.050.000.000.30954.141174.812001488902
10sample0.050.000.000.26955.351128.432001488902
11sample0.050.000.000.26972.691083.602001488902
12sample0.050.000.000.261032.971138.762001488702
13sample0.050.000.000.29918.461087.542001488802
14sample0.030.000.000.20914.301087.612001488802
15sample0.050.000.000.28911.341079.162001488802
16sample0.040.000.000.22911.521085.192001488602
17sample0.050.000.000.23927.141072.572001488902
18sample0.050.000.000.27948.961197.742001488302
19sample0.050.000.000.27926.941100.802001488902
20sample0.020.000.000.11998.451120.072001488702
21sample0.050.000.000.27927.041083.122001488902
22sample0.040.000.000.17905.791077.852001488802
23sample0.050.000.000.28906.701079.682001488902

Public page /jobs, new connection each time​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.14237.97295.92296.10934.16935.562001506812
2warmup2.1393.38197.68197.821128.401130.562001506712
3warmup2.73213.04264.35264.54870.59872.112001506612
4sample1.88201.36408.93409.151404.781407.072001506712
5sample3.82212.681325.481325.752073.422074.682001506712
6sample2.14213.06617.80618.011586.611593.342001506712
7sample3.1224.4452.9753.06662.65663.952001506712
8sample3.4723.9851.9852.08823.87825.942001506712
9sample3.1423.6749.8749.98655.19657.112001506712
10sample3.10208.75394.33394.551279.921289.362001506712
11sample3.1024.0952.5852.70709.68713.612001506612
12sample3.0122.0154.7154.81772.07774.282001506712
13sample2.08177.19362.51362.761288.991293.322001506612
14sample3.06171.90273.02273.191222.251223.652001506812
15sample2.84204.17487.40487.671965.632055.792001506612
16sample3.30144.25485.62485.851535.021597.792001509812
17sample2.52175.36421.14421.481506.831508.852001506612
18sample2.1421.6949.0249.10686.76688.802001506712
19sample2.14233.30440.12440.271825.451838.332001506712
20sample3.19201.80459.40459.571579.791589.132001506712
21sample3.02212.55592.03592.251559.541560.942001506612
22sample3.1223.3853.9154.01659.17660.872001506712
23sample1.87201.83401.73401.961301.731311.362001506512

Public page /jobs, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.2122.1648.5548.62646.10647.162001506712
2warmup0.020.000.000.14588.95591.752001506702
3warmup0.030.000.000.17687.31688.962001506802
4sample0.140.000.000.34595.53596.042001506602
5sample0.010.000.000.07580.52583.942001506702
6sample0.020.000.000.10607.05610.592001506502
7sample0.050.000.000.22591.46592.322001506602
8sample0.030.000.000.14593.04594.082001506502
9sample0.040.000.000.20689.43691.522001506602
10sample0.050.000.000.20593.17594.042001506802
11sample0.030.000.000.16592.97594.612001506602
12sample0.050.000.000.23664.59666.042001506802
13sample0.050.000.000.23591.06595.352001506702
14sample0.020.000.000.11692.72694.142001506602
15sample0.050.000.000.27671.27673.402001506702
16sample0.050.000.000.22597.39599.162001506702
17sample0.020.000.000.09613.14614.592001506702
18sample0.050.000.000.26730.88732.332001506702
19sample0.050.000.000.24671.67673.042001506502
20sample0.040.000.000.23656.19656.992001506702
21sample0.030.000.000.14764.30767.512001506602
22sample0.060.000.000.23589.04590.392001506702
23sample0.050.000.000.22609.55610.292001506702

Public page /employers/dashboard, signed in, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup1.98102.29299.97300.24816.49820.05200751112
2warmup0.030.000.000.14386.20387.29200751202
3warmup0.040.000.000.26370.94374.91200750902
4sample0.040.000.000.21406.69409.80200751002
5sample0.030.000.000.15396.10397.44200751102
6sample0.050.000.000.26522.95523.78200751102
7sample0.030.000.000.17568.14569.41200751202
8sample0.060.000.000.32782.82786.62200751102
9sample0.050.000.000.30375.32379.49200751202
10sample0.030.000.000.13383.34386.58200751102
11sample0.030.000.000.23390.39391.54200751202
12sample0.050.000.000.26376.34381.30200751102
13sample0.030.000.000.18436.37437.53200751202
14sample0.050.000.000.281477.821489.87200748602
15sample0.050.000.000.33417.76444.73200748602
16sample0.050.000.000.261063.971068.14200751102
17sample0.050.000.000.27840.05843.54200751102
18sample0.050.000.000.27417.47418.81200751202
19sample0.050.000.000.28407.87411.41200751302
20sample0.040.000.000.19387.44391.26200751102
21sample0.050.000.000.25560.94568.39200751102
22sample0.030.000.000.14463.19464.33200751002
23sample0.040.000.000.25426.46427.75200751202