A subdomain label has to pass two tests. The protocol test is mechanical: length, characters, and encoding, and it takes minutes. The reader test lasts as long as the name does: what the label promises, who answers for it, and how it ends. Get the first out of the way with real tools, then spend your effort on the second.
The hard limits, tested with dig
| Rule | Limit | Source |
|---|---|---|
| Label length | 1 to 63 octets | RFC 1035, section 2.3.4 |
| Whole name | 255 octets on the wire, which is 253 characters as typed | RFC 1035, section 2.3.4 |
| Hostname characters | Letters, digits, and hyphens; a label must not start or end with a hyphen | RFC 1035, section 2.3.1 |
| First character | A letter or a digit (the original rule allowed only letters) | RFC 1123, section 2.1 |
| Letter case | No significance: API and api are the same label |
RFC 1035, section 2.3.1 |
-- in the third and fourth positions |
Reserved; only xn-- is in use, and it must be valid Punycode |
RFC 5890, section 2.3.1 |
The gap between 255 and 253 comes from the wire format: every label carries a length byte, and the name ends with an empty root label, which adds two octets to the dotted text you type.
Live capture, 26 September 2026. DiG 9.10.6 on macOS, querying names at and just past each limit under the reserved .test suffix:
$ label63=$(printf 'a%.0s' {1..63}); label64=${label63}a
$ dig +nocmd @1.1.1.1 "$label63.example.test" A +noall +comments | grep status
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 15606
$ dig @1.1.1.1 "$label64.example.test" A; echo "exit status: $?"
dig: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.test' is not a legal name (label too long)
exit status: 10
$ name253="$label63.$label63.$label63.$(printf 'b%.0s' {1..56}).test"; name254="$label63.$label63.$label63.$(printf 'b%.0s' {1..57}).test"
$ echo ${#name253} ${#name254}
253 254
$ dig +nocmd @1.1.1.1 "$name253" A +noall +comments | grep status
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 41867
$ dig @1.1.1.1 "$name254" A; echo "exit status: $?"
dig: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.test' is not a legal name (ran out of space)
exit status: 10
The 63-character label and the 253-character name both left the machine and came back NXDOMAIN, the correct answer for anything under .test. The 64-character label and the 254-character name never left: dig rejected them locally and exited with status 10. Other software fails differently. A DNS dashboard may reject the record, a certificate order may fail on one name, and a web form may truncate without saying so, so “the provider accepted it” is not proof that every client will.
Legal is not the same as usable. A 63-character label is valid and still painful in logs, dashboards, and certificate name lists. The hostname checker tests label syntax, per-label and total length, and punycode conversion before you create anything.
One Unicode name, two DNS names
DNS itself stores only ASCII. An internationalized label is converted to an ASCII form beginning with xn-- (an A-label) before lookup, and the conversion rules changed between IDNA 2003 and IDNA 2008.
Live capture, 26 September 2026. Converting the same inputs with Python 3.13’s built-in codec, libidn2 2.3.8, and Node 26’s URL parser:
$ python3 -c "print('bücher.example'.encode('idna'))"
b'xn--bcher-kva.example'
$ python3 -c "print('straße.example'.encode('idna'))"
b'strasse.example'
$ idn2 straße.example
xn--strae-oqa.example
$ idn2 bücher.example
xn--bcher-kva.example
$ node -e "console.log(new URL('https://straße.example/').hostname)"
xn--strae-oqa.example
$ node -e "console.log(new URL('https://BÜCHER.example/').hostname)"
xn--bcher-kva.example
For bücher, all three tools agree, even from uppercase input. For straße, they do not. Python documents its built-in idna codec as an implementation of RFC 3490 (IDNA 2003) and points to the third-party idna package for IDNA 2008. IDNA 2003 maps ß to ss; libidn2 and Node’s WHATWG URL parser keep ß and encode it. Unicode’s UTS #46 (version 18.0.0, dated 31 August 2026) calls ß, the Greek final sigma ς, and the zero-width joiner and non-joiner “deviation” characters, says the industry has fully moved to the IDNA 2008 behavior for them, and deprecates the old transitional mapping. A script built on the older codec therefore looks up a different host than a browser does.
For public subdomains:
- Use ASCII labels for anything operational: APIs, callbacks, dashboards, and anything typed into configuration.
- If a Unicode label is a product requirement, publish the
xn--form in DNS, test it in the software your users actually run, and avoid the four deviation characters. - Never create a label with
--in the third and fourth positions unless it is a valid A-label; RFC 5890 reserves that pattern.
Underscore labels are for records, not hosts
Live capture, 26 September 2026. Our own DMARC policy lives at an underscore label, and a URL parser accepts that label as a hostname:
$ dig +short @1.1.1.1 _dmarc.and.guide TXT
"v=DMARC1;p=quarantine;rua=mailto:admin@and.guide"
$ node -e "console.log(new URL('https://_dmarc.and.guide/').hostname)"
_dmarc.and.guide
DNS has no objection to _, and neither did the URL parser. The restriction lives in the hostname rules, and it is deliberate: RFC 8552 explains that underscored names such as _dmarc, _domainkey, and _tcp can be told apart from every legal host name precisely because host names cannot contain underscores. The practical break comes at HTTPS. CA/Browser Forum Ballot SC012 required that after 30 April 2019, underscores must not appear in the DNS names of publicly trusted certificates, so _api.example.com cannot be listed in one.
Keep underscores for scoped records (DMARC, DKIM, SRV, ACME challenge TXT records) and use hyphens in anything a browser or API client connects to.
Labels that promise authority
Some labels make a claim before anyone loads the page:
login,auth,account,sso: identity is handled herebilling,pay,checkout: money moves herestatus: current, maintained incident informationsupport,help: someone will answeradmin,internal,vpn: privileged access- a vendor or product name: an official relationship
Reserve these until the function exists and a team accepts ownership of it. A placeholder at status.example.com is worse than no status host, because people check it during exactly the incident it fails to describe. On a shared domain, keep a short registry of reserved labels and delegated prefixes, so two teams do not publish docs, developer-docs, and help-docs for the same audience.
Name the purpose, not the tool
The Certificate Transparency history of our own domain shows how tool-based names accumulate. Since May 2021, publicly trusted certificates have named 113 distinct hostnames under and.guide, and many of the labels describe the software that happened to serve them: wp, wp2static, static-elementor, graphql. Of those 113, 77 were last certified before 2025, and every one of them remains in public logs; the subdomain inventory runbook walks through that data.
Permanent names should describe what a visitor gets (docs, api, status, handbook) so they survive a change of platform. Temporary names should carry their scope and owner under one parent:
pr-482.preview.example.com
release-2026-09.preview.example.com
client-a.review.example.com
A shared parent gives you one place for access rules and one branch to clean up. If the platform serves a wildcard certificate for that parent instead of issuing one certificate per name, individual preview names also stay out of Certificate Transparency logs. The trade-off is that every invented name under a wildcard resolves, so the default route has to fail closed; the wildcard DNS and TLS guide covers both sides. Avoid bare test, temp, or new: they announce that a host is temporary without saying whose it is or when it ends.
Approve a name with an owner and an exit
Before the DNS record exists, write down the facts that will outlive the person who asked for it:
Hostname: docs.example.com
Purpose: product documentation for customers
Owner: documentation team (DNS changes approved by platform team)
Destination: static hosting project, CNAME target from the provider
Access: public, indexable
Lifecycle: permanent, reviewed yearly
Retirement action: 301 to the replacement, or 410 if the content is gone
Then run three checks:
- Syntax and length, with the hostname checker or with dig as shown above.
- Existence, by asking the authoritative name server for the label. Under a wildcard, a new label already resolves, so an answer does not mean the label is taken, and you will never see
NXDOMAINas a sign that it is free. - Destination and certificate plan: record type, platform binding, and HTTPS, as described in the subdomain setup guide.
A naming system works when every public name has an owner, a purpose its label describes accurately, and a recorded way to end.