- name
- node-response-time-review
- description
- Analyze production Node.js app container response times to find slow-rendering sites, cross-checking against nginx queuing delay to rule out false positives (a site only looks slow because the event loop was blocked by a different, pathological site). Use when asked to look for slow sites, investigate response times, or re-run the node response time analysis.
# Node.js response time review → problem-site identification
Reproduces the workflow from issue #1825: aggregate per-domain render times
across the app containers, flag anything crossing the concern threshold, and
distinguish genuinely slow sites from sites merely queued behind one.
**Concern threshold: 150ms.** Any single render over 150ms is worth a look;
a site is worth flagging when a meaningful share of its requests exceed it,
or its *average* render time is elevated (avg >150ms means most requests
are slow, not just an occasional GC pause / cold cache).
This skill only **identifies** problem sites — it does not investigate root
causes (why a given site is slow) unless separately asked to.
## 1. Source of the technique
The `upstream` alias in `~/.bashrc` on the prod host (`ssh blot`) live-tails
`access.log` for `st=$upstream_response_time`:
```bash
alias upstream='tail -f /var/instance-ssd/logs/access.log | stdbuf -oL grep "st=[^-]" | stdbuf -oL awk "{print \$10, \$3, \$4, \$7}"'
```
That's a live view of nginx's *upstream* wait time — useful for watching
traffic in real time, but it conflates two different things: the app
actually being slow to render a site, vs. the app's single-threaded event
loop being busy with a *different* concurrent request when this one arrived
(queuing delay). At the ~15s nginx proxy timeout, both look identical from
nginx's side alone. So this skill goes one step further and cross-checks
against the app's own logged render time per request.
## 2. Get the data
SSH host is `blot`. **Always confirm with the user before running anything
against production**; stick to read-only commands.
nginx access log (for cross-checking / a broader traffic sample, and to see
504s / true timeouts that never got a completion line in the app log):
```bash
ssh blot "wc -l /var/instance-ssd/logs/access.log"
```
Log format (see `config/openresty/conf/http.conf` `log_format
access_log_format`): space-separated, `$7` = full URL, `$10` = `st=` value
(comma-separated if the request was retried across upstreams — sum the
parts). `st=-` means no upstream was contacted (pure cache hit) — skip
those lines. Exclude blot.im's own long-lived endpoints, which are
long-polling by design, not bugs:
```
^https://blot\.im/sites/[^/]+/status (publish-status long poll)
^https://webhooks\.blot\.im/ (webhook delivery)
/draft/stream/ (live preview SSE)
```
App container logs (the authoritative source for "did the app itself take
a long time to render this"). Check container uptime first — a recent
deploy/restart limits how far back `--since` can usefully go:
```bash
ssh blot "docker ps --format '{{.Names}}\t{{.Status}}'"
for c in blue green yellow; do
ssh blot "docker logs --since 6h blot-container-$c 2>&1"
done > /tmp/app_logs_combined.txt
```
Completed-request lines look like:
`[12/Sep/2026:09:29:23 +0000] [yellow] <request-id> 200 0.013 https://www.example.com/path`
— fields: `$3`=`[color]`, `$4`=request id, `$5`=status, `$6`=render time
(seconds), `$7`=url. A request that hangs past nginx's timeout never gets
this line — instead you'll see `<request-id> Connection closed by client
<url>` once nginx gives up. That's itself a strong signal: search for it
directly (`grep "Connection closed by client"`) to catch sites whose worst
requests are too slow to even produce a normal timing line.
## 3. Aggregate per domain
```bash
awk '
{
if ($3 ~ /^\[(blue|green|yellow)\]$/ && $5 ~ /^[0-9]+$/ && $6 ~ /^[0-9]+\.[0-9]+$/ && $7 ~ /^https?:\/\//) {
t = $6 + 0; url = $7;
n = split(url, u, "/"); domain = u[3];
if (domain == "") next;
count[domain]++; sum[domain] += t;
if (t > max[domain]) max[domain] = t;
if (t > 0.15) over150[domain]++;
if (t > 1) over1s[domain]++;
}
}
END {
for (d in count)
printf "%s\tcount=%d\tavg=%.4f\tmax=%.3f\tover150ms=%d\tover1s=%d\tpct150=%.1f\n",
d, count[d], sum[d]/count[d], max[d], over150[d]+0, over1s[d]+0, (over150[d]+0)*100/count[d]
}' /tmp/app_logs_combined.txt > /tmp/app_domain_stats.txt
```
Rank by average render time and by `pct150` (share of requests over the
150ms bar), filtering out low-traffic domains (`count < 20` or so — too
noisy to draw conclusions from a handful of requests):
```bash
awk -F'\t' '{
for(i=1;i<=NF;i++){split($i,kv,"="); v[kv[1]]=kv[2]}
if (v["count"]+0 >= 20) print v["avg"], v["count"], v["pct150"]+0, v["max"], $1
}' /tmp/app_domain_stats.txt | sort -rn | head -40
```
## 4. Classify each candidate
For every domain that crosses the threshold, characterize *how* it's slow —
this determines severity and whether it's a false positive:
- **High avg AND high pct150 AND high max** (e.g. nashp.com: avg 3.8s, 75%
>150ms, max 17s) → genuinely, severely slow. Real bug, high priority.
- **High pct150 but low max, avg well above 150ms but well below 1s** (e.g.
chuckpearson.blog: avg 0.78s, 87% >150ms, max only 1.7s) → consistently
*moderately* slow on nearly every request, not occasional spikes. Also
real, but a different shape of problem (steady overhead vs. pathological
worst case) — worth noting as such.
- **Moderate pct150, avg comfortably under 150ms** (e.g. karaman.is: avg
0.44s pulled up by a handful of outliers, but most requests fast) →
borderline; note it but don't treat as confirmed without a second look.
- **High nginx-level `st=` numbers but the SAME domain's app-log render
times are almost all fast** (e.g. www.markwadley.com: nginx avg 3s / 25%
>3s, but app-log avg 0.24s / 12% >150ms with no correlation) → **false
positive**. The slowness measured at nginx is queuing delay from a
different concurrent request blocking the event loop, not this site's
own cost. Don't flag it as the site's problem.
- **No completed app-log lines at all, but nginx shows requests and/or
`Connection closed by client` for the domain** (e.g. anchor.blot.im) →
the app never finished rendering within the proxy timeout at all. This is
a severe finding even with a tiny sample size — a single hang beyond 15s
matters more than a moderate average.
Always sanity-check a top candidate against `TODO` in the repo root — some
slow sites are already known/tracked (e.g. "Fix performance issues with
nashp", "Fix issue with warwickmostyn").
## 5. Report
Don't file a new issue every run — this is a recurring check. Update the
existing tracking issue (currently
[#1825](https://github.com/blotcms/blot/issues/1825)) with the
current window's numbers via `gh issue edit` (replace body) or `gh issue
comment` (append), rather than creating a duplicate. Structure the update
as: threshold/method recap, confirmed slow sites (with the avg/pct150/max
numbers and the app-vs-nginx evidence), false positives ruled out, and
anything low-confidence that needs another pass. Keep root-cause
speculation out unless asked — this skill is about identification only.
Clean up scratch files on both ends when done:
`ssh blot "rm -f /tmp/app_logs_combined.txt"` and remove any local
`/tmp/app_domain_stats.txt` equivalents.
GitHub에서 보기