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:46:23Z
Finished (UTC)2026-09-01T14:49:37Z
Run time189 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, connection reused: the page did not stream. Time to first byte (340.57 ms) is almost the whole response (345.58 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 (1522.24 ms) is almost the whole response (1543.62 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 (929.85 ms) is almost the whole response (936.65 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 /employers/dashboard, signed in, connection reused: the page did not stream. Time to first byte (521.32 ms) is almost the whole response (525.98 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)84.87626.69570.921227.951230.55
Whole response (time_total)85.23628.04572.041229.121231.38
DNS (time_namelookup)1.822.572.633.403.66
TCP connect (time_connect)22.50160.16132.63214.06269.61
TLS handshake (time_appconnect)48.81274.38251.67470.05552.56
Ready to send (time_pretransfer)48.92274.56251.82470.28552.78

All values are milliseconds.

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

RequestTTFBWhole responseStatus
1995.46996.84200
2179.55181.14200
31236.741238.05200

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)30.2731.7835.8440.07102.95
Whole response (time_total)31.0132.8036.6240.86103.95
DNS (time_namelookup)0.020.030.030.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.120.200.200.290.31

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
1164.77165.16200
231.0331.30200
330.9531.95200

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)97.37477.31458.05868.171191.92
Whole response (time_total)97.72478.21458.97869.211193.30
DNS (time_namelookup)1.932.192.473.243.26
TCP connect (time_connect)22.27135.34124.26213.27213.48
TLS handshake (time_appconnect)50.30260.72253.04429.58692.78
Ready to send (time_pretransfer)50.42260.94253.23429.82693.03

All values are milliseconds.

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

RequestTTFBWhole responseStatus
1738.30739.57200
294.5294.84200
3107.48107.80200

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)198.69239.33272.91417.34462.67
Whole response (time_total)214.27260.10331.27711.60862.65
DNS (time_namelookup)0.020.040.040.060.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.140.250.240.310.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
1728.61730.08200
2236.81237.27200
3235.75236.88200

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)91.81790.33829.982021.673099.64
Whole response (time_total)92.19878.86872.272025.083101.05
DNS (time_namelookup)1.912.742.753.294.99
TCP connect (time_connect)22.32175.32134.39239.81259.73
TLS handshake (time_appconnect)48.04311.77291.63609.18616.91
Ready to send (time_pretransfer)48.12312.02291.80609.28617.14

All values are milliseconds.

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

RequestTTFBWhole responseStatus
11542.391543.90200
2204.72206.31200
3139.73141.16200

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)337.30340.57346.16367.50387.23
Whole response (time_total)341.50345.58349.92371.54391.29
DNS (time_namelookup)0.020.040.040.060.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.120.210.210.270.28

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
11066.381066.71200
2339.83344.12200
3342.77346.62200

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)475.651180.431132.671526.231643.19
Whole response (time_total)477.711331.241257.581736.991805.62
DNS (time_namelookup)2.112.852.823.233.32
TCP connect (time_connect)22.00193.29166.94214.34236.00
TLS handshake (time_appconnect)55.08401.18359.35488.74603.32
Ready to send (time_pretransfer)55.18401.39359.59488.99603.53

All values are milliseconds.

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

RequestTTFBWhole responseStatus
11755.111964.42200
2484.10486.62200
3711.13729.20200

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)674.08711.85733.05820.511046.98
Whole response (time_total)805.37832.77875.471126.171197.40
DNS (time_namelookup)0.030.050.050.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.200.260.260.310.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
11145.491296.76200
2723.57808.80200
3696.05824.76200

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)802.121522.241478.031972.582371.64
Whole response (time_total)803.661543.621519.262137.742378.71
DNS (time_namelookup)1.932.732.653.173.40
TCP connect (time_connect)22.07193.13173.56239.19282.08
TLS handshake (time_appconnect)49.33401.79354.93484.52680.43
Ready to send (time_pretransfer)49.45402.10355.12484.62680.64

All values are milliseconds.

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

RequestTTFBWhole responseStatus
11829.121837.79200
21760.411899.54200
3822.27827.85200

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)568.49929.851010.251322.092308.00
Whole response (time_total)579.90936.651027.151332.652308.83
DNS (time_namelookup)0.010.040.040.050.09
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.210.200.280.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
11197.241199.46200
2933.65944.19200
31049.491051.18200

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)430.58521.32669.141247.131728.58
Whole response (time_total)433.93525.98677.351254.181735.31
DNS (time_namelookup)0.020.050.050.060.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.150.270.270.410.44

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
1960.74961.90200
2604.96606.17200
31040.881042.95200

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?239.33 ms, median, on a connection that is already open
How much of that is network, Cloudflare and TLS?31.78 ms, median, the /up floor
How much of that is Rails work?207.55 ms
What does that real call cost when the connection is new?477.31 ms, median, including DNS, TCP and TLS
What does opening the connection add?237.98 ms
How much of the network floor is DNS?2.57 ms, median
How much of it is TCP?160.16 ms, median
How much of it is TLS?274.38 ms, median
What does the page shell cost on its own?345.58 ms, median, for /up
What does a light public page cost?832.77 ms, median, for /privacy
What does a public list page cost?936.65 ms, median, for /jobs
What does the signed-in employer dashboard cost?525.98 ms, median

Read it like this.

  • One real API call from the browser costs about 239.33 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.
  • About 207.55 ms of that is Rails work. The rest is network, Cloudflare and TLS.
  • The network on its own costs about 31.78 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 237.98 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 345.58 ms. The data arrives about 239.33 ms after that. Total: about 584.91 ms before the page is complete.
  • A loader() page has one wait. /jobs takes about 936.65 ms, and it arrives complete.
  • On a first visit, /jobs costs about 606.97 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.31185.81377.80377.98995.46996.842008212
2warmup2.2822.3151.6151.67179.55181.142008212
3warmup3.40193.65476.03476.231236.741238.052008212
4sample3.2355.11114.26114.38762.23762.902008212
5sample2.4522.5851.4551.5394.7698.232008212
6sample2.73104.60215.56215.73594.16595.542008212
7sample2.6923.0548.8148.9284.8785.232008212
8sample2.01177.18360.81361.07971.64972.552008212
9sample2.06191.49392.20392.431227.951229.122008212
10sample2.91192.20392.26392.45790.87792.222008212
11sample2.15176.46291.92292.12491.96493.152008212
12sample3.23183.71378.59378.711101.701103.102008212
13sample3.3590.62121.51121.62172.45172.972008212
14sample2.04183.78395.65395.85740.49740.882008212
15sample2.25143.85283.82283.99667.97669.362008212
16sample3.35210.83258.07258.23295.13296.282008212
17sample3.0724.1650.6850.77147.25147.802008212
18sample3.66214.06552.56552.781230.551231.382008212
19sample1.82130.29285.20285.32659.22660.532008212
20sample2.09212.34264.93265.13314.26315.422008212
21sample2.10269.61470.05470.28879.24881.982008212
22sample3.4024.2855.3155.41104.77105.132008212
23sample2.0222.5049.6649.7286.8487.122008212

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.3196.93125.66125.75164.77165.162008212
2warmup0.020.000.000.0731.0331.302008202
3warmup0.010.000.000.0730.9531.952008202
4sample0.070.000.000.3130.8131.822008202
5sample0.030.000.000.2130.6631.482008202
6sample0.030.000.000.2132.7033.552008202
7sample0.030.000.000.2931.6432.952008202
8sample0.030.000.000.2531.7932.662008202
9sample0.030.000.000.1732.5833.032008202
10sample0.020.000.000.1731.0431.882008202
11sample0.030.000.000.2033.0933.892008202
12sample0.030.000.000.1933.3433.862008202
13sample0.020.000.000.16102.95103.952008202
14sample0.050.000.000.2731.7632.192008202
15sample0.020.000.000.1430.7531.702008202
16sample0.070.000.000.2630.2731.012008202
17sample0.030.000.000.1830.6331.162008202
18sample0.020.000.000.1430.8131.362008202
19sample0.020.000.000.1232.1933.192008202
20sample0.030.000.000.2132.2333.052008202
21sample0.030.000.000.2036.0836.962008202
22sample0.030.000.000.1640.0740.862008202
23sample0.030.000.000.1831.3431.802008202

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.09189.13385.38385.63738.30739.5720018212
2warmup3.5023.8548.4448.5494.5294.8420018212
3warmup1.9921.7648.7248.79107.48107.8020018212
4sample1.9322.37132.10132.29192.83194.0420018212
5sample2.88200.92429.58429.82823.04824.4320018212
6sample2.13213.48262.43262.69319.02320.3120018212
7sample3.0423.4951.7151.84107.61108.0820018212
8sample2.07105.97135.67135.82191.81193.1120018212
9sample2.79139.94292.71292.96534.59535.7220018212
10sample2.15174.48353.56353.73688.90689.6520018212
11sample2.1822.2751.9752.06154.99156.4220018212
12sample2.8323.9650.3050.4297.3797.7220018212
13sample1.96130.73428.99429.26659.72660.5320018212
14sample3.24177.02287.87288.03490.41490.9520018212
15sample2.53213.27259.01259.19312.50313.0020018212
16sample2.86120.33249.76249.91498.78499.3920018212
17sample2.20192.08393.84394.03868.17869.2120018212
18sample2.01213.26692.78693.031191.921193.3020018212
19sample2.1922.3555.2755.36111.24111.6020018212
20sample1.98170.94268.78268.97464.21465.4720018212
21sample2.16202.18418.27418.51816.99818.3220018212
22sample3.2692.84191.07191.32526.13527.1920018212
23sample3.0423.2955.2355.33110.72111.0620018212

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.17151.97308.41308.65728.61730.0820018212
2warmup0.050.000.000.29236.81237.2720018202
3warmup0.020.000.000.18235.75236.8820018202
4sample0.060.000.000.29233.94234.7220018202
5sample0.030.000.000.25462.67463.6920018202
6sample0.050.000.000.31241.49242.4120018202
7sample0.030.000.000.25417.34418.3220018202
8sample0.050.000.000.29236.60237.5020018202
9sample0.040.000.000.26235.21236.2520018202
10sample0.040.000.000.28261.29261.7020018202
11sample0.020.000.000.15238.72862.6520018202
12sample0.020.000.000.20411.21412.4020018202
13sample0.050.000.000.28290.44291.5120018202
14sample0.040.000.000.26239.82258.5020018202
15sample0.030.000.000.20272.40273.6320018202
16sample0.060.000.000.33237.75244.4420018202
17sample0.040.000.000.23269.25270.8320018202
18sample0.040.000.000.23235.71242.6420018202
19sample0.020.000.000.14285.02285.4320018202
20sample0.020.000.000.18238.83711.6020018202
21sample0.040.000.000.25198.69214.2720018202
22sample0.040.000.000.23216.51222.6120018202
23sample0.040.000.000.22235.33240.2920018202

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup3.04134.34975.63975.881542.391543.90200474212
2warmup3.0198.98130.72130.87204.72206.31200474212
3warmup3.0624.5657.0057.09139.73141.16200474212
4sample2.6122.6649.6249.69178.89179.68200474212
5sample2.92212.97492.26492.491326.341327.43200474312
6sample2.00212.21616.91617.143099.643101.06200474312
7sample2.1022.7658.6558.73186.15189.63200474212
8sample3.1023.2049.1649.2698.9199.36200474312
9sample2.49173.32358.92359.102021.672025.08200474212
10sample4.9926.9055.2055.29112.57113.06200474212
11sample2.06239.81421.49421.671055.451056.55200474212
12sample2.69189.68309.97310.28713.12715.08200474212
13sample3.10177.32366.19366.411019.361020.84200474212
14sample3.14143.78293.89294.10713.30888.92200474012
15sample3.20214.15483.03483.24871.631516.94200474212
16sample2.63214.49609.18609.281211.841213.51200474012
17sample3.2923.0551.1451.24103.48103.94200474112
18sample1.91107.60270.44270.63480.15481.21200474212
19sample2.04189.08476.43476.64867.37868.80200474212
20sample3.1822.3248.0448.12129.11129.60200474112
21sample1.98259.73460.44460.721382.891385.02200474112
22sample2.79190.17313.56313.76935.94937.51200474212
23sample2.8622.6548.1348.2491.8192.19200474112

Public page /up, the control, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup1.96186.30379.31379.551066.381066.71200474212
2warmup0.010.000.000.06339.83344.12200474102
3warmup0.040.000.000.18342.77346.62200474102
4sample0.080.000.000.24339.86343.68200474102
5sample0.040.000.000.17340.49345.77200474202
6sample0.050.000.000.22339.77343.58200474102
7sample0.040.000.000.19342.31346.90200474102
8sample0.060.000.000.27339.89344.17200474102
9sample0.030.000.000.21337.98342.08200474202
10sample0.050.000.000.23338.55341.50200474202
11sample0.050.000.000.21341.58346.46200474202
12sample0.040.000.000.20338.21341.97200474202
13sample0.030.000.000.16339.92344.12200474202
14sample0.050.000.000.22359.75360.81200474102
15sample0.040.000.000.20340.65345.88200474202
16sample0.050.000.000.22342.23345.72200474202
17sample0.040.000.000.21351.27352.89200474102
18sample0.050.000.000.22341.09345.44200474102
19sample0.040.000.000.21337.30341.78200474202
20sample0.020.000.000.12358.80360.93200474302
21sample0.050.000.000.28338.74341.96200474302
22sample0.040.000.000.19367.50371.54200474102
23sample0.040.000.000.18387.23391.29200474202

Public page /privacy, new connection each time​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup1.82189.31400.59400.841755.111964.422001488912
2warmup2.2324.1653.9754.05484.10486.622001486112
3warmup2.9052.65109.87109.99711.13729.202001489012
4sample3.17236.00430.51430.921128.251286.982001481212
5sample3.32140.49286.53286.741178.121707.422001488812
6sample2.23121.55309.17309.53889.94960.442001489012
7sample2.85201.66427.26427.511462.971471.772001486212
8sample3.05214.34453.93454.311257.111736.992001488812
9sample3.06203.47400.71400.891147.431277.892001489012
10sample2.15192.88463.52463.791643.191805.622001488412
11sample2.6522.0055.0855.18475.65477.712001486312
12sample2.80191.92390.95391.201234.861324.122001488812
13sample2.17198.74412.78413.041182.751338.352001488812
14sample2.8481.21109.65109.80590.07592.902001486512
15sample3.23192.29393.51393.771526.231528.102001486012
16sample3.17200.56488.74488.991409.401421.052001486312
17sample3.17211.12482.71482.921119.281147.582001488712
18sample2.77202.36461.58461.801224.511391.332001488812
19sample2.5987.56116.64116.71672.99675.132001486112
20sample2.11209.16603.32603.531514.461545.982001488912
21sample3.1960.91129.36129.52679.20700.322001489012
22sample3.20193.70401.64401.901227.311540.292001488812
23sample2.77176.96369.50369.681089.611221.612001488512

Public page /privacy, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.03180.71447.52447.771145.491296.762001488312
2warmup0.050.000.000.25723.57808.802001488902
3warmup0.050.000.000.28696.05824.762001488802
4sample0.030.000.000.20738.17875.142001489002
5sample0.040.000.000.22678.49833.302001488602
6sample0.050.000.000.29693.09832.242001488802
7sample0.050.000.000.25741.60842.662001488402
8sample0.050.000.000.29750.63945.172001488602
9sample0.050.000.000.31693.68827.432001488602
10sample0.050.000.000.27747.79823.562001489002
11sample0.030.000.000.20721.16854.132001488702
12sample0.050.000.000.25680.86831.052001488802
13sample0.040.000.000.28717.49856.882001488902
14sample0.060.000.000.24689.90820.762001489002
15sample0.040.000.000.23752.94811.302001488602
16sample0.050.000.000.26682.66831.992001489002
17sample0.050.000.000.29706.20821.292001488802
18sample0.050.000.000.27690.07907.622001488902
19sample0.050.000.000.27678.24828.422001489002
20sample0.050.000.000.33820.511126.172001488602
21sample0.050.000.000.28674.08805.372001488802
22sample0.050.000.000.201046.981197.402001488702
23sample0.050.000.000.24756.52837.502001488702

Public page /jobs, new connection each time​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.04185.66386.12386.391829.121837.792001506612
2warmup2.86198.38338.94339.141760.411899.542001506712
3warmup2.7622.7048.6248.72822.27827.852001506612
4sample1.9395.74201.08201.341313.041318.742001506512
5sample2.0222.07125.83125.93913.35915.162001506812
6sample3.4023.2949.3349.45802.12803.662001506612
7sample2.13211.28680.43680.642371.642378.712001506812
8sample3.09191.70392.17392.371545.021552.262001506612
9sample2.64213.35366.24366.391346.091488.112001506712
10sample2.48126.79193.11193.361174.551177.422001506712
11sample2.18194.54484.52484.621603.431611.672001506512
12sample2.00190.83406.60406.781972.582137.742001509712
13sample3.07213.78444.97445.131533.041534.982001506612
14sample2.82191.71390.25390.441967.622132.772001509912
15sample3.11239.19429.96430.231868.952012.332001509912
16sample3.17282.08478.56478.681577.091592.472001506612
17sample2.8626.9153.6353.73839.38840.942001506512
18sample2.10188.38411.11411.321606.251611.142001506712
19sample2.64211.12328.25328.371095.231096.292001506612
20sample2.88237.33439.85440.011511.441645.402001509112
21sample3.13191.63396.98397.411501.141509.442001506812
22sample2.21213.22412.20412.361344.421349.762001506712
23sample3.17206.17413.60413.831674.171676.142001506712

Public page /jobs, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup1.87146.12249.12249.321197.241199.462001506612
2warmup0.050.000.000.22933.65944.192001506602
3warmup0.040.000.000.181049.491051.182001506702
4sample0.090.000.000.281021.461023.082001506702
5sample0.050.000.000.23919.65921.302001506602
6sample0.050.000.000.23913.22924.782001506602
7sample0.010.000.000.061018.681019.732001506702
8sample0.040.000.000.20927.39928.902001506602
9sample0.050.000.000.30901.00913.572001506802
10sample0.030.000.000.19952.171028.582001502502
11sample0.050.000.000.22920.17931.412001506602
12sample0.040.000.000.18901.79909.562001506802
13sample0.030.000.000.18932.30933.712001506702
14sample0.050.000.000.23908.361048.232001505202
15sample0.050.000.000.231322.091332.652001506702
16sample0.050.000.000.25919.79930.142001506602
17sample0.050.000.000.23598.35608.192001506802
18sample0.010.000.000.06942.99953.772001506702
19sample0.030.000.000.15937.25939.592001506802
20sample0.050.000.000.211305.481316.072001506502
21sample0.040.000.000.15568.49579.902001506602
22sample0.040.000.000.202308.002308.832001506702
23sample0.030.000.000.17986.25991.032001506702

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup1.84194.56439.52439.68960.74961.90200751212
2warmup0.030.000.000.20604.96606.17200751102
3warmup0.050.000.000.261040.881042.95200751002
4sample0.050.000.000.291728.581735.31200751302
5sample0.050.000.000.28449.36455.84200751102
6sample0.020.000.000.15436.15443.01200751102
7sample0.040.000.000.25430.58433.93200751102
8sample0.050.000.000.25522.38523.69200751202
9sample0.060.000.000.33432.07525.09200751202
10sample0.050.000.000.30520.26526.88200751102
11sample0.070.000.000.41843.09849.71200751202
12sample0.050.000.000.25542.78544.20200751302
13sample0.050.000.000.44508.12510.38200751302
14sample0.050.000.000.28513.95515.01200751102
15sample0.040.000.000.261247.131254.18200751202
16sample0.060.000.000.27505.94508.36200751202
17sample0.050.000.000.26831.04836.36200751202
18sample0.040.000.000.23888.30889.49200751102
19sample0.050.000.000.28898.64900.19200751102
20sample0.030.000.000.16624.23625.43200751102
21sample0.050.000.000.28437.81443.23200751002
22sample0.040.000.000.22495.99498.31200750902
23sample0.040.000.000.24526.39528.38200751102