How We Built Our Own DNS Server β€” CoPilot Blog
    Neura MarketNeura Market/CoPilot
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityCoPilotCoPilot
    DeepSeekDeepSeekStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityPluginsTrendingGenerate
    CoPilotBlogHow We Built Our Own DNS Server
    Back to Blog
    How We Built Our Own DNS Server
    cloud

    How We Built Our Own DNS Server

    Jonas Scholz April 17, 2026
    0 views

    We wrote a production DNS server in ~1000 lines of Go, migrated thousands of records off Hetzner DNS,...

    We wrote a production DNS server in ~1000 lines of Go, migrated thousands of records off Hetzner DNS, and dropped propagation time from "up to 90 minutes" to a few seconds. It uses the hidden primary pattern, Postgres as the event bus, and AXFR + IXFR to push zones to public secondaries. Here's how and why we did it! ![gif](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExMDFoeHV4MHd1cHJpdG8zMXFndjg1ejBxbjV1bnJsN2p1djZubHE4eCZlcD12MV9naWZzX3RyZW5kaW5nJmN0PWc/WRQBXSCnEFJIuxktnw/giphy.gif) ## Why Hetzner DNS Stopped Working for Us Every service on Sliplane gets a managed subdomain like my-app-abc123.sliplane.app. That means an A and AAAA record for every running service, pointing at the server IP where the container lives. Records scale linearly with the platform. We started with Hetzner DNS because it was free and we already ran most of our infra there. That worked fine for a while, but after 2 years we hit two walls: **Record limits**: Hetzner DNS has a hard cap per zone. Originally 500, they bumped it to 10k for us (genuinely appreciate that), but at our growth rate we'd blow through that within weeks. Apparently we're one of their biggest DNS users by record count \:D **Speed**: After creating a record via the API, it could take up to 90 minutes before Hetzner's own nameservers actually returned it. For a PaaS where someone just deployed a service and wants to visit the URL, that's a rough experience. Although this wasn't consistently that bad, everytime that happened it directly affected the user experience. It simply looked like our platform was broken (which in that case it was!). ## Why Not Just Use Another Managed Provider? Fair question. For most people, a managed DNS provider is the right answer. But once you start shopping around at our "scale" and constraints, things get annoying fast: **"Contact sales" pricing.** A lot of the providers that could comfortably handle our record count sit behind "talk to sales" forms. I hate that. Just tell me what it costs. **Per-record or per-query billing.** The ones that do publish pricing often charge per record or per query. We have no idea how many DNS queries we actually serve, so migrating to an unknown pricing model felt like signing a blank check. **EU-only.** We're based in the EU and wanted to keep DNS there too. That narrows the field a lot. **And honestly, it sounded fun.** I'm a bit of a controlfreak and writing a DNS server is the kind of thing you daydream about. A thousand lines of Go felt worth the freedom. In the end, building the thing took less time than getting a meeting with a managed provider would have πŸ˜΅β€πŸ’« So we built it ourselves, which brings us to the pattern that made it surprisingly simple. ## The Hidden Primary Pattern The reason why this is all way simpler than I initially thought: our DNS server never answers a single public query. In DNS, a zone's primary nameserver holds the authoritative records. Secondaries pull copies using [AXFR](https://en.wikipedia.org/wiki/DNS_zone_transfer) (basically a full zone dump over TCP) and answer public queries just like the primary would. When the primary changes, it sends a NOTIFY to the secondaries, and they pull a fresh copy. A **hidden primary** takes this one step further, the primary isn't public at all. It only exists to push zone data to secondaries. The public nameservers, the ones listed at your registrar, are all secondaries. This means we can run our DNS server wherever we want, use any secondary provider that supports [AXFR](https://en.wikipedia.org/wiki/DNS_zone_transfer), and swap providers without changing our server. No lock-in because AXFR and NOTIFY are standard protocols, any compliant secondary will work. No anycast, no super redundant ddos protected DNS servers deployed across the globe. Just a few instances of our primary hidden server. ## The Architecture ![Architecture diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1xctjqp3au78ral1xr2r.png) The setup is pretty minimal: **Postgres is the source of truth.** We install triggers that call `pg_notify('dns_zone_changed', '')` whenever a service is created, updated, or deleted. No message queue, no webhooks. Postgres *is* the event bus. Why not Redis, NATS, or a proper queue? Two reasons. We already run Postgres as our primary database, so `LISTEN`/`NOTIFY` is "free" (no free lunch, but as free as it gets) infrastructure, nothing new to operate, monitor, or pay for. And the volume is tiny. Zone changes happen a few times per minute at peak, which is laughably low for anything queue-shaped. Reaching for Kafka here would be like renting a shipping container to mail a postcard. **sliplane-dns** is a small Go server (~1000 lines, built on [miekg/dns](https://codeberg.org/miekg/dns)) that subscribes via `LISTEN`, queries Postgres for all managed domains and their IPs, builds the DNS zone, and serves it via AXFR. To avoid unnecessary work, we hash all records. If the hash matches the previous zone, nothing happens, no serial bump, no NOTIFY. When the zone actually changes, we bump the SOA serial and send DNS NOTIFY to Hetzner's three secondary IPs. They pull the new zone, and records are live. To see what a zone transfer actually looks like, here's a minimal DNS server that only speaks AXFR. It serves a hardcoded zone for `example.com` with a single A record ([full code on GitHub](https://github.com/code42cate/dns-axfr-sample)): ```go package main import ( "context" "log" "net/netip" "codeberg.org/miekg/dns" "codeberg.org/miekg/dns/rdata" ) func main() { soa := &dns.SOA{ Hdr: dns.Header{Name: "example.com.", TTL: 3600, Class: dns.ClassINET}, SOA: rdata.SOA{Ns: "ns1.example.com.", Mbox: "admin.example.com.", Serial: 1}, } records := []dns.RR{ soa, &dns.A{ Hdr: dns.Header{Name: "app.example.com.", TTL: 300, Class: dns.ClassINET}, A: rdata.A{Addr: netip.MustParseAddr("1.2.3.4")}, }, soa, } mux := dns.NewServeMux() mux.HandleFunc("example.com.", func(_ context.Context, w dns.ResponseWriter, r *dns.Msg) { r.Unpack() w.Hijack() env := make(chan *dns.Envelope, len(records)) for _, rr := range records { env <- &dns.Envelope{Answer: []dns.RR{rr}} } close(env) dns.NewClient().TransferOut(w, r, env) w.Close() }) srv := dns.NewServer() srv.Addr = ":5553" srv.Net = "tcp" srv.Handler = mux log.Fatal(srv.ListenAndServe()) } ``` Run it and pull the zone with dig: ```bash dig @localhost -p 5553 example.com AXFR ``` ```conf example.com. 3600 IN SOA ns1.example.com. admin.example.com. 1 0 0 0 0 app.example.com. 300 IN A 1.2.3.4 example.com. 3600 IN SOA ns1.example.com. admin.example.com. 1 0 0 0 0 ``` The full zone transfer is just SOA, all records, SOA again. This is roughly what Hetzner's secondaries pull from our production server, just with a few thousand more records in between the two SOAs. ## Saturday Night DNS Surgery You can't gradually migrate DNS nameservers. The NS records at the registrar point to either the old set or the new set. There's a cutover window, no way around it. We had to switch from Hetzner's nameservers (`hydrogen.ns.hetzner.com`, `oxygen.ns.hetzner.com`, `helium.ns.hetzner.de`) to Hetzner Robot's secondary nameservers (`ns1.first-ns.de`, `robotns2.second-ns.de`, `robotns3.second-ns.com`). During the transition, resolvers with cached old NS records would still ask the old servers and get stale data until TTL expired. Two things made this manageable: the NS delegation TTL was 5 minutes, and only *new* services deployed during that window were affected. Existing A/AAAA records were identical on both sets of nameservers. We did it on a Saturday night when platform activity was lowest. It went smooth, no users complained! ## The One Thing That Bit Us: IXFR I went into this thinking AXFR was enough. It's the protocol every tutorial shows, every example uses, and it's what I built first. Full zone dump, SOA at the start, SOA at the end, done. Turns out Hetzner Robot's secondaries don't just do AXFR. When they already have a zone and see a new SOA serial via NOTIFY, they ask for an *incremental* zone transfer first ([IXFR, RFC 1995](https://www.ietf.org/archive/id/draft-ah-dnsext-rfc1995bis-ixfr-02.html)), a diff of only the records that changed since the old serial. If the primary doesn't speak IXFR, a well-behaved secondary falls back to AXFR. Hetzner Robot apparently doesn't fall back cleanly in every case, so zones weren't updating reliably until we implemented IXFR too. IXFR isn't hard, you just keep a small history of recent zone versions and, on request, return the delta between the client's serial and the current one. But it's the kind of thing you'd only discover by actually shipping it against a real secondary. Cheers to whoever wrote that RFC. ## Was It Worth It? So far, 100%. Propagation went from "up to 90 minutes" to however long it takes to do a zone transfer, which for our zone size is practically instant. The zone grows with the platform without hitting any record ceilings, and we also have full observability baked in. ## Should You Do This? Probably not. Use Cloudflare DNS, Route 53, or whatever managed DNS your provider offers. They're fast, they work, and you don't have to think about them. But if you *do* end up hitting the limits of a managed DNS provider, the hidden primary pattern is worth knowing about. Your primary doesn't need to be public, you can use any AXFR-compatible secondary, and you can swap providers without touching your server. Cheers, Jonas, Co-Founder [sliplane.io](https://sliplane.io?utm_source=we-built-our-own-dns-server)

    Tags

    clouddevopsdockerwebdev

    Comments

    More Blog

    View all
    Minimalist EKS: The Easy Waykubernetes

    Minimalist EKS: The Easy Way

    Amazon EKS manages the Kubernetes control plane, but you remain responsible for provisioning the...

    J
    Joaquin Menchaca
    Never forget to enter the Stern Grove lottery again!ai

    Never forget to enter the Stern Grove lottery again!

    Browser automation with Playwright, Python, GitHub Actions, and Entire to auto-enter San Francisco Stern Grove concert lotteries each week!

    L
    Lizzie Siegle
    A Free Screenshot Editor That Never Uploads Your Imagetypescript

    A Free Screenshot Editor That Never Uploads Your Image

    A free screenshot and image editor that runs entirely in your browser. Keeping every edit reversible and handling big phone photos, in plain TypeScript and Canvas2D.

    M
    Martin Stark
    I built a CLI to break my highlights out of Apple Booksshowdev

    I built a CLI to break my highlights out of Apple Books

    A macOS CLI + MCP server that exports Apple Books highlights to Markdown and gives AI assistants direct access to your reading notes.

    A
    Andrey Korchak
    A Developer's Guide to Agent Hooks in Antigravity CLIai

    A Developer's Guide to Agent Hooks in Antigravity CLI

    Motivation To be quite honest, "Hooks"β€”the shell commands we trigger at specific points...

    T
    Tanaike
    Tactical vs. Strategic Agentic AI Development β€” A Playbook for Developersagents

    Tactical vs. Strategic Agentic AI Development β€” A Playbook for Developers

    The Strategic Engineer: Why Writing Code Is No Longer Your Most Valuable Skill ...

    A
    Adewumi Saheed Adewale

    Stay up to date

    Get the latest CoPilot prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for CoPilot and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    Β© 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.