its-not-dns
v0.1.0
Published
"It's not DNS. There's no way it's DNS. It was DNS." — the haiku, as a diagnostic. npx its-not-dns yoursite.com walks the blame chain (hosts file → resolvers → records → TCP → TLS → HTTP) and prints the verdict. Compares your system resolver against 1.1.1
Maintainers
Readme
its-not-dns
It's not DNS.There's no way it's DNS.It was DNS.
The haiku, as a diagnostic.
npx its-not-dns yoursite.com"The site is down" hits the channel. Someone says it loads fine for them. Someone blames the CDN. Someone restarts the pod. Forty minutes later it's a stale DNS record, like it always was.
its-not-dns walks the whole blame chain in order — hosts file → system
resolver vs 1.1.1.1 vs 8.8.8.8 → records & CNAME chain → TCP to every
resolved IP → TLS certificate → HTTP redirects — and ends with a verdict, not a
data dump:
api.example.com:443 (https)
“It's not DNS.”
✓ no /etc/hosts override
✓ A @system: 104.20.23.154, 172.66.147.243 (3 ms)
✓ A @1.1.1.1: 104.20.23.154, 172.66.147.243 (5 ms)
✓ A @8.8.8.8: 104.20.23.154, 172.66.147.243 (36 ms)
✓ getaddrinfo: 104.20.23.154, 172.66.147.243 (44 ms)
“There's no way it's DNS.”
✓ tcp 104.20.23.154:443 (35 ms)
✓ tcp 172.66.147.243:443 (39 ms)
✗ tls: CERT_EXPIRED
“It wasn't DNS. It was the certificate.”
The TLS certificate expired 41 day(s) ago (Apr 12 23:59:59 2026 GMT).
fix: Renew the cert / check the auto-renewal job that silently died.And when it is DNS — and it's always DNS — you get to paste this in the channel:
“It was DNS.”What it actually catches
| Layer | The classics it names |
| ----- | --------------------- |
| hosts file | The /etc/hosts pin from an incident six months ago, still shadowing prod for every app on your laptop |
| Resolvers | NXDOMAIN; split-horizon (corp DNS says one thing, 1.1.1.1 another); mid-propagation changes; system cache vs raw DNS (getaddrinfo disagreeing with the wire) |
| Records | Domains with no A/AAAA left; CNAME chains; v6-only/v4-only mismatches ("curl works, the app doesn't") |
| TCP | All resolved IPs dead (that's an outage, not DNS) — or only some dead, the "works every other refresh" special |
| TLS | Expired certs (with day count), not-yet-valid certs (clock skew), hostname mismatches (SANs listed), self-signed chains |
| HTTP | Redirect loops (CDN ↔ origin https confusion), 5xx, slow first byte |
One culprit wins — by the priority order of what actually breaks in practice — and every verdict comes with a one-line fix.
Install
npx its-not-dns yoursite.com # no install
npm i -g its-not-dns # keep it for the next incidentNode ≥ 18. Zero runtime deps beyond the CLI niceties (cac, picocolors).
No API key, no service, no telemetry — the only traffic is to your target
and the public resolvers being compared.
Usage
its-not-dns api.example.com # full chain, https:443
its-not-dns http://10.0.0.5:3000/health # URLs, ports, paths
its-not-dns shop.example.com --resolver 10.0.0.2 # also ask your corp DNS (split-horizon hunting)
its-not-dns api.example.com --no-http # stop after DNS/TCP/TLS
its-not-dns api.example.com --md incident.md # markdown for the postmortem
its-not-dns api.example.com --json | jq .verdict
it-was-dns api.example.com # same tool — for when you already knowExit codes: 0 = everything checks out · 1 = found a culprit · 2 = usage
error. So you can even use it as a smoke test:
- run: npx its-not-dns api.example.com || echo "::warning::$(npx its-not-dns api.example.com --md -)"Real scenarios
1. "It's down for customers but fine for us." Run it where it's broken (or
have the customer run it — it's an npx one-liner). Resolver disagreement and
hosts-file pins show up instantly, with the line number.
2. The 3 a.m. cert. ✗ tls: CERT_EXPIRED — expired 0 day(s) ago is a
different conversation than "the site is down". The verdict names the layer
before anyone restarts anything.
3. Split-horizon hunting. --resolver 10.0.0.2 adds your corp/VPN DNS to
the comparison table. When it answers differently than 1.1.1.1, you've found
your "works off VPN" bug.
Library API
The verdict engine is pure — feed it findings from anywhere (tests do):
import { analyze, parseTarget, findHostsOverrides } from "its-not-dns";
const diagnosis = analyze(findings); // findings: what the probes gathered
diagnosis.verdict; // { wasDns: true, culprit: "resolver-disagreement", line: "It was DNS.", fix: "…" }
diagnosis.lines; // the ✓/✗ lines under each verseRoadmap
--watch— re-run until the verdict changes (TTL countdowns during failover).- DNSSEC validation check;
--doh(DNS-over-HTTPS) comparison. - MX/TXT modes for "my email is down" (
its-not-dns --mx example.com). - A
--comparemode: run on two machines, diff the verdicts (womm-style).
💖 Sponsor
Free, MIT, built in spare time. If this ended a blame-storm before it started:
- ⭐ Star the repo — so the next on-call finds it at 3 a.m.
- 🍋 Sponsor via Lemon Squeezy — one-time or recurring.
License
MIT © its-not-dns contributors
