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-01T09:56:19Z
Finished (UTC)2026-09-01T09:57:41Z
Run time81 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 sentjod-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​

None. Every request answered 200, no response came from a cache, and every connection behaved as expected.

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)85.2196.57180.88397.99450.14
Whole response (time_total)85.5296.86181.80398.52451.50
DNS (time_namelookup)1.842.122.162.693.02
TCP connect (time_connect)21.7124.2341.5294.28115.06
TLS handshake (time_appconnect)47.5653.8990.80192.99197.03
Ready to send (time_pretransfer)47.6253.9590.91193.22197.30

All values are milliseconds.

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

RequestTTFBWhole responseStatus
197.5197.74200
2306.54307.77200
3103.16103.55200

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)105.48111.56125.10192.72193.50
Whole response (time_total)106.08112.50126.12193.75194.31
DNS (time_namelookup)0.010.030.030.040.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.170.160.230.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
1240.02241.27200
2335.90336.97200
3165.26166.26200

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)94.05142.49198.40457.64561.01
Whole response (time_total)94.41143.38199.16458.94562.39
DNS (time_namelookup)1.902.322.493.203.25
TCP connect (time_connect)22.0323.5447.99137.09165.39
TLS handshake (time_appconnect)48.4852.7996.23216.16279.64
Ready to send (time_pretransfer)48.5552.8796.36216.35279.85

All values are milliseconds.

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

RequestTTFBWhole responseStatus
1116.97117.34200
2243.09244.36200
3317.09318.34200

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)106.81113.25129.77189.67192.03
Whole response (time_total)107.35113.85130.47190.64192.67
DNS (time_namelookup)0.010.030.030.040.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.060.180.180.290.32

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
1296.38297.80200
2109.82110.87200
3118.94119.82200

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)95.94171.91187.79309.27463.14
Whole response (time_total)96.15172.24188.59310.78464.50
DNS (time_namelookup)1.752.162.363.403.74
TCP connect (time_connect)21.3723.2743.60103.57105.49
TLS handshake (time_appconnect)46.4088.8998.82148.83294.77
Ready to send (time_pretransfer)46.4689.0198.92148.93295.03

All values are milliseconds.

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

RequestTTFBWhole responseStatus
1246.76247.03200
2100.56100.92200
3109.76110.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)38.1440.7348.29109.88128.22
Whole response (time_total)38.4541.4449.13113.15128.47
DNS (time_namelookup)0.010.010.010.020.02
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.070.070.100.11

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
1146.20146.59200
239.8140.19200
343.2743.61200

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)445.61498.26569.70773.50990.81
Whole response (time_total)446.57500.23571.51777.83992.79
DNS (time_namelookup)1.732.142.433.333.37
TCP connect (time_connect)21.3123.2543.22101.91131.82
TLS handshake (time_appconnect)47.2556.6198.25213.91297.37
Ready to send (time_pretransfer)47.3256.6998.36214.11297.59

All values are milliseconds.

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

RequestTTFBWhole responseStatus
1442.60444.95200
2901.06902.34200
3488.51490.55200

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)502.39515.80524.87561.72621.68
Whole response (time_total)508.98522.17531.19570.39622.88
DNS (time_namelookup)0.020.050.040.060.10
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.090.210.200.260.27

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
1874.70876.26200
2521.07524.04200
3523.51525.29200

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)532.96652.95713.891032.181371.90
Whole response (time_total)534.97654.59716.191036.151377.09
DNS (time_namelookup)1.742.652.583.133.41
TCP connect (time_connect)21.7423.9746.53104.31108.26
TLS handshake (time_appconnect)48.1552.3391.27206.39217.58
Ready to send (time_pretransfer)48.2452.4291.39206.60217.82

All values are milliseconds.

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

RequestTTFBWhole responseStatus
1548.45549.35200
2550.86551.36200
3533.66537.15200

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)475.09520.27536.72569.54982.29
Whole response (time_total)476.40523.17538.80571.03983.91
DNS (time_namelookup)0.010.050.050.070.16
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.220.210.310.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
1525.72526.36200
2498.70499.14200
3473.03474.46200

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)80.5186.4896.34168.30180.90
Whole response (time_total)81.7187.3197.58171.97183.31
DNS (time_namelookup)0.010.040.040.120.18
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.210.190.300.39

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
1142.36142.91200
284.8586.12200
390.6791.89200

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?113.25 ms, median, on a connection that is already open
How much of that is network, Cloudflare and TLS?111.56 ms, median, the /up floor
How much of that is Rails work?1.69 ms
What does that real call cost when the connection is new?142.49 ms, median, including DNS, TCP and TLS
What does opening the connection add?29.24 ms
How much of the network floor is DNS?2.12 ms, median
How much of it is TCP?24.23 ms, median
How much of it is TLS?53.89 ms, median
What does the page shell cost on its own?41.44 ms, median, for /up
What does a light public page cost?522.17 ms, median, for /privacy
What does a public list page cost?523.17 ms, median, for /jobs
What does the signed-in employer dashboard cost?87.31 ms, median

Read it like this.

  • One real API call from the browser costs about 113.25 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 1.69 ms of that is Rails work. The rest is network, Cloudflare and TLS.
  • The network on its own costs about 111.56 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 29.24 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 41.44 ms. The data arrives about 113.25 ms after that. Total: about 154.69 ms before the page is complete.
  • A loader() page has one wait. /jobs takes about 523.17 ms, and it arrives complete.
  • On a first visit, /jobs costs about 131.42 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 '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 '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 '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 '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 '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 '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 '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.0027.1055.3255.3897.5197.742008212
2warmup1.9259.68133.05133.14306.54307.772008212
3warmup2.8723.3051.9352.03103.16103.552008212
4sample2.1194.28197.03197.30378.11379.422008212
5sample2.1492.27192.99193.22450.15451.502008212
6sample2.1222.8649.0449.1189.8890.222008212
7sample2.0624.5651.6151.6893.8894.152008212
8sample1.9724.3855.9155.9899.2599.572008212
9sample2.23115.06191.90192.24317.55318.792008212
10sample2.1757.32119.16119.25397.99398.522008212
11sample2.1256.58117.76117.88220.73222.062008212
12sample3.0224.08143.28143.47180.31181.442008212
13sample2.2322.2747.9848.0586.9087.322008212
14sample2.0022.4451.0951.1591.4691.722008212
15sample1.9829.3958.1558.21186.82188.172008212
16sample2.1721.7248.0548.1285.2185.522008212
17sample2.0121.9448.3948.4590.6190.932008212
18sample1.8756.91121.83121.93250.00251.092008212
19sample2.3323.6649.7649.8388.6294.002008212
20sample2.1022.4648.8148.8786.8887.202008212
21sample2.0352.62123.82123.93249.07249.602008212
22sample2.6923.6651.8751.9388.8989.172008212
23sample1.8422.0147.5647.6285.3685.612008212

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.5559.24125.26125.36240.02241.272008212
2warmup0.040.000.000.18335.90336.972008202
3warmup0.040.000.000.21165.26166.262008202
4sample0.070.000.000.28111.45112.312008202
5sample0.040.000.000.20108.89109.732008202
6sample0.030.000.000.23111.75117.552008202
7sample0.040.000.000.19181.70183.472008202
8sample0.040.000.000.17107.36108.342008202
9sample0.030.000.000.21112.28112.692008202
10sample0.020.000.000.16109.05110.132008202
11sample0.040.000.000.18192.72193.752008202
12sample0.040.000.000.19111.68112.742008202
13sample0.030.000.000.19105.58106.082008202
14sample0.020.000.000.09106.72107.722008202
15sample0.030.000.000.22193.50194.312008202
16sample0.020.000.000.14123.01123.302008202
17sample0.010.000.000.08124.26124.522008202
18sample0.010.000.000.06124.24124.802008202
19sample0.020.000.000.09149.86150.492008202
20sample0.020.000.000.12108.53108.882008202
21sample0.010.000.000.06107.83108.182008202
22sample0.020.000.000.08106.14107.002008202
23sample0.040.000.000.18105.48106.512008202

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.1340.2568.4768.58116.97117.3420018212
2warmup2.2259.51125.00125.11243.09244.3620018212
3warmup4.05118.03190.97191.22317.09318.3420018212
4sample3.1456.15117.34117.50283.78285.5020018212
5sample3.1323.9549.5649.6797.2997.8120018212
6sample2.2756.56119.44119.58241.49242.7420018212
7sample2.5789.06116.31116.45158.79160.1820018212
8sample3.2023.9249.6049.7294.0594.4120018212
9sample1.9056.95120.23120.34561.01562.3920018212
10sample3.2023.1748.5948.7198.1198.4820018212
11sample2.02165.39279.64279.85457.65458.9420018212
12sample3.25137.09201.48201.64314.62315.1920018212
13sample1.9822.6452.6452.73126.20126.5820018212
14sample2.3622.80135.93136.23179.42180.6220018212
15sample3.0724.1663.3963.52114.17114.5520018212
16sample2.6722.6452.9353.02101.03101.4120018212
17sample1.9722.3848.4848.55180.47180.8420018212
18sample2.0522.1949.9550.0397.5797.8620018212
19sample1.94101.39216.16216.35377.86378.8720018212
20sample2.1722.3050.6650.75100.75101.1020018212
21sample1.9522.0351.4851.57105.35105.6920018212
22sample1.9722.0549.7349.80168.71170.0220018212
23sample2.9823.0551.0251.13109.71110.0820018212

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.1354.10113.03113.12296.38297.8020018212
2warmup0.030.000.000.29109.82110.8720018202
3warmup0.040.000.000.25118.94119.8220018202
4sample0.030.000.000.27109.41110.7320018202
5sample0.030.000.000.21181.34182.1420018202
6sample0.040.000.000.25108.47109.2920018202
7sample0.040.000.000.29113.91114.8520018202
8sample0.040.000.000.26112.40113.3820018202
9sample0.030.000.000.21184.46185.2120018202
10sample0.030.000.000.18112.72113.2320018202
11sample0.020.000.000.13106.81107.3520018202
12sample0.020.000.000.13114.34114.7420018202
13sample0.010.000.000.09189.67190.6420018202
14sample0.060.000.000.27107.33107.8220018202
15sample0.020.000.000.12112.82113.5720018202
16sample0.030.000.000.17107.02107.8720018202
17sample0.030.000.000.19192.03192.6720018202
18sample0.020.000.000.15109.99110.5220018202
19sample0.020.000.000.13117.74118.8820018202
20sample0.040.000.000.32116.36116.6420018202
21sample0.010.000.000.07176.32176.5920018202
22sample0.010.000.000.06113.67114.1320018202
23sample0.020.000.000.09108.67109.1220018202

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.3956.75122.10122.18246.76247.03200424012
2warmup1.8923.8453.5053.58100.56100.92200424012
3warmup1.9922.6350.5650.63109.76110.16200424012
4sample2.2821.8951.2351.30129.32129.64200424012
5sample2.01103.57129.77129.85180.86182.13200424012
6sample1.8360.45126.94127.04286.71288.69200424012
7sample2.2596.27123.81123.90172.41172.74200424012
8sample1.8621.5551.0751.1495.9496.15200424012
9sample1.8321.4250.7450.79116.25116.70200424012
10sample2.5623.30148.83148.93233.36233.86200424012
11sample2.6222.9649.7649.85104.99105.47200423912
12sample2.6721.3747.6447.72100.56102.34200423912
13sample3.7423.25146.49146.64193.99195.28200424012
14sample2.9822.7050.5350.59100.22100.69200423912
15sample2.0722.4854.1354.21104.46104.88200423912
16sample1.9022.2448.4948.56171.40171.73200424012
17sample1.8064.57129.47129.56255.14255.88200424012
18sample1.7552.36123.66123.80288.71289.88200424012
19sample2.6924.5450.1250.2099.91100.37200424012
20sample2.01105.49294.77295.03463.14464.50200424012
21sample3.4062.77128.55128.65309.27310.78200424012
22sample2.8657.22124.05124.19246.75247.42200424012
23sample2.0321.6946.4046.46102.36102.66200424012

Public page /up, the control, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.0253.6582.1882.25146.20146.59200424012
2warmup0.010.000.000.0639.8140.19200424002
3warmup0.010.000.000.0943.2743.61200424002
4sample0.020.000.000.0840.9841.35200424002
5sample0.010.000.000.0738.2738.67200424002
6sample0.010.000.000.0843.2343.74200424002
7sample0.010.000.000.1041.3643.43200424002
8sample0.010.000.000.0938.4738.84200424002
9sample0.020.000.000.08128.22128.47200424002
10sample0.010.000.000.0539.8640.12200424002
11sample0.010.000.000.0542.6342.87200424002
12sample0.010.000.000.0539.8640.23200424002
13sample0.010.000.000.0739.3340.56200424002
14sample0.010.000.000.0741.0641.54200424002
15sample0.010.000.000.0939.8443.40200424002
16sample0.010.000.000.0738.5139.18200424002
17sample0.010.000.000.0638.1438.45200424002
18sample0.010.000.000.0740.8341.96200424002
19sample0.010.000.000.0640.6240.89200424002
20sample0.010.000.000.05109.88113.15200424002
21sample0.020.000.000.1142.7243.05200424002
22sample0.010.000.000.0738.5938.94200424002
23sample0.020.000.000.0743.3943.72200424002

Public page /privacy, new connection each time​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup1.9121.7347.5347.62442.60444.952001433312
2warmup3.00104.54215.91216.15901.06902.342001433412
3warmup3.2025.1265.0365.14488.51490.552001433412
4sample3.0859.16122.84122.94604.49604.892001433112
5sample1.7322.7549.8849.94447.93449.902001432912
6sample2.7066.00126.15126.26615.85616.882001433412
7sample2.0822.3848.6648.74458.62459.782001433212
8sample2.0522.3448.6648.74445.61446.572001433112
9sample1.9621.8647.2547.32448.13448.702001433012
10sample2.2123.2047.9848.04456.18458.112001433212
11sample3.1993.41204.57204.70990.81992.792001433012
12sample3.3323.4750.7750.86474.11474.602001432812
13sample2.07131.82297.37297.59701.88703.392001433512
14sample1.9622.4460.2960.38520.83522.762001432912
15sample2.1521.8056.7756.87498.02500.002001433112
16sample2.17101.91213.91214.11773.50777.832001433012
17sample2.1321.3147.8947.95498.49500.462001433312
18sample3.1023.3049.3749.47471.30472.822001433312
19sample2.0122.2248.0248.11447.51449.392001433412
20sample3.3757.26118.49118.63615.25618.852001433412
21sample2.0860.68131.34131.46695.93698.762001433212
22sample3.1124.40138.42138.56745.36747.562001433512
23sample2.1222.6556.4456.51484.26486.212001433312

Public page /privacy, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.33101.98212.27212.51874.70876.262001433212
2warmup0.030.000.000.15521.07524.042001433302
3warmup0.050.000.000.22523.51525.292001433102
4sample0.100.000.000.27521.26522.512001433302
5sample0.040.000.000.16621.68622.882001433502
6sample0.050.000.000.26513.79518.122001433602
7sample0.040.000.000.19509.64516.042001433302
8sample0.050.000.000.21507.29514.212001433502
9sample0.050.000.000.22505.98511.392001433102
10sample0.050.000.000.19517.91523.582001433402
11sample0.060.000.000.21516.32524.002001433602
12sample0.020.000.000.10515.28518.852001433102
13sample0.040.000.000.17512.73519.732001433302
14sample0.030.000.000.13537.88544.942001433302
15sample0.040.000.000.16529.80538.982001433002
16sample0.050.000.000.23510.17521.832001433202
17sample0.050.000.000.24552.01559.322001433202
18sample0.030.000.000.22510.35518.252001433502
19sample0.050.000.000.21502.39508.982001433102
20sample0.050.000.000.19519.48527.392001433002
21sample0.050.000.000.21511.51518.962001433302
22sample0.050.000.000.21520.17523.542001433202
23sample0.020.000.000.09561.72570.392001433402

Public page /jobs, new connection each time​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.5822.5848.2848.37548.45549.352001499112
2warmup2.3122.6449.5649.64550.86551.362001499112
3warmup2.1824.1657.3257.39533.66537.152001499112
4sample2.9524.13144.28144.51661.43663.472001499112
5sample2.6523.1358.2258.29598.00599.142001499112
6sample2.6522.5749.9850.05540.91542.882001499312
7sample2.0697.75206.39206.601032.181036.152001499112
8sample2.1424.3152.7952.88535.83536.832001499212
9sample2.8323.8148.3748.43541.00541.752001499112
10sample1.7421.7450.4850.54532.96534.972001499312
11sample3.0123.3749.6449.73536.28538.702001499212
12sample3.1397.97126.40126.53732.08734.402001499212
13sample2.8523.0851.1351.22899.27901.352001499012
14sample2.2522.2148.6748.74543.00547.652001499212
15sample3.0625.2051.8751.97625.49627.662001499212
16sample2.1722.2251.5451.63834.84836.782001499012
17sample2.15108.26217.58217.82825.97831.272001499112
18sample2.19101.99131.38131.50644.48645.702001499112
19sample3.4156.69118.12118.30741.75742.842001499312
20sample2.4121.8848.1548.25591.53592.662001499112
21sample2.92104.31142.40142.531371.901377.092001499012
22sample2.2122.3648.8748.97788.51790.612001499112
23sample2.7463.54129.16129.29700.37701.842001499112

Public page /jobs, connection reused​

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup1.8521.3047.4647.53525.72526.362001499112
2warmup0.010.000.000.06498.70499.142001499402
3warmup0.010.000.000.06473.03474.462001499102
4sample0.070.000.000.26484.21486.102001499002
5sample0.050.000.000.26482.88484.612001499102
6sample0.050.000.000.27475.09476.402001499002
7sample0.040.000.000.17480.00481.442001499002
8sample0.050.000.000.22569.54571.032001499102
9sample0.050.000.000.29479.86481.462001499102
10sample0.040.000.000.21982.29983.912001499302
11sample0.060.000.000.25514.80518.292001499002
12sample0.010.000.000.05526.22528.482001499102
13sample0.020.000.000.11523.91525.332001499102
14sample0.160.000.000.35519.81523.772001499202
15sample0.050.000.000.21524.07526.072001499102
16sample0.050.000.000.24517.63520.352001499202
17sample0.050.000.000.31566.21570.122001499102
18sample0.040.000.000.20535.64538.272001499202
19sample0.050.000.000.22481.94482.932001499102
20sample0.030.000.000.15503.28506.302001499002
21sample0.030.000.000.14522.71523.562001499002
22sample0.030.000.000.15523.51524.852001499102
23sample0.050.000.000.23520.72522.782001499002

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

RequestKindDNSTCPTLSReady to sendTTFBWhole responseStatusBytesConnectionsHTTP
1warmup2.1022.5049.6449.73142.36142.91200699012
2warmup0.020.000.000.0984.8586.12200698902
3warmup0.040.000.000.3090.6791.89200698902
4sample0.040.000.000.25121.01122.14200698902
5sample0.030.000.000.2186.8687.47200699002
6sample0.020.000.000.1184.6385.26200698802
7sample0.030.000.000.1395.6996.60200698902
8sample0.020.000.000.1981.5982.82200698902
9sample0.050.000.000.30168.30171.97200698902
10sample0.010.000.000.0581.5081.71200699002
11sample0.010.000.000.0583.9384.97200698802
12sample0.020.000.000.1285.2985.86200698802
13sample0.020.000.000.1186.5887.25200698802
14sample0.030.000.000.14180.90183.31200699002
15sample0.180.000.000.3983.1785.94200698802
16sample0.040.000.000.2189.4790.59200699002
17sample0.030.000.000.2580.5181.82200698802
18sample0.040.000.000.2481.7083.11200698902
19sample0.120.000.000.2886.4587.37200699002
20sample0.030.000.000.2292.4093.48200699002
21sample0.040.000.000.2387.3388.31200698902
22sample0.040.000.000.2182.9684.03200698902
23sample0.030.000.000.2286.5287.60200699002