Tech Stack Detection in Go: Build or Buy Guide

April 17, 2026 · 10 min read

Go is a natural fit for tech stack detection. Its concurrency model lets you run DNS lookups, HTTP fetches, and TLS inspection in parallel. Its standard library has everything you need for HTTP clients, DNS resolution, and JSON parsing. And if you’re building detection into a larger pipeline—a security scanner, a sales enrichment tool, or an infrastructure audit system—Go compiles to a single binary with no runtime dependencies.

This guide walks through two approaches: building detection from scratch using the open-source wappalyzergo library, and calling the DetectZeStack API from Go. We’ll compare what each approach gives you, what each one misses, and help you decide which makes sense for your use case.

Why Go for Tech Stack Detection

Concurrency and Performance

Tech stack detection is inherently I/O-bound. For every domain you scan, you need to:

  1. Resolve DNS records (CNAME chains, NS records, MX records)
  2. Fetch the HTTP response (headers, HTML body, status code)
  3. Inspect the TLS certificate (issuer, subject, SANs)
  4. Match all of the above against fingerprint databases

In Go, you can run steps 1–3 concurrently with goroutines and collect the results on channels. A batch scan of 100 domains that would take minutes sequentially finishes in seconds when each domain gets its own goroutine. This is exactly how DetectZeStack’s backend works internally—DNS and HTTP fetch run in parallel for every request.

DNS and HTTP in the Standard Library

Go’s net package handles DNS resolution natively. You can look up CNAME records with net.LookupCNAME, query nameservers with net.LookupNS, and resolve IPs with net.LookupHost. The net/http package gives you a full HTTP client with redirect following, timeout control, and TLS configuration. No external dependencies needed for the transport layer.

Building Tech Stack Detection in Go from Scratch

Fetching and Parsing HTTP Headers

The simplest starting point is fetching a URL and examining the HTTP response. Here’s a minimal example that extracts the Server, X-Powered-By, and other revealing headers:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Get("https://stripe.com")
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    defer resp.Body.Close()

    interesting := []string{
        "Server", "X-Powered-By", "X-Generator",
        "CF-Ray", "X-Vercel-Id", "X-Amz-Cf-Id",
    }
    for _, h := range interesting {
        if v := resp.Header.Get(h); v != "" {
            fmt.Printf("%s: %s\n", h, v)
        }
    }
}

This catches low-hanging fruit. If a site runs Nginx, Cloudflare, or Vercel, the Server header often says so directly. But header-only detection misses the majority of a site’s stack—JavaScript frameworks, analytics tools, CMS platforms, and CDN providers that don’t set identifying headers.

Matching Technologies with wappalyzergo

The wappalyzergo library (maintained by ProjectDiscovery) is a Go port of the Wappalyzer fingerprint database. It matches HTTP headers, cookies, HTML content, and JavaScript patterns against 7,300+ technology signatures. Here’s how to use it:

package main

import (
    "fmt"
    "io"
    "net/http"
    "time"

    wappalyzer "github.com/projectdiscovery/wappalyzergo"
)

func main() {
    wap, _ := wappalyzer.New()

    client := &http.Client{Timeout: 15 * time.Second}
    resp, err := client.Get("https://github.com")
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)

    // Fingerprint returns map[string]struct{} of detected tech names
    techs := wap.Fingerprint(resp.Header, body)
    fmt.Printf("Detected %d technologies on github.com:\n", len(techs))
    for tech := range techs {
        fmt.Printf("  - %s\n", tech)
    }
}

Install the library with:

go get github.com/projectdiscovery/wappalyzergo@latest

This detects technologies like React, jQuery, Nginx, Google Analytics, WordPress, Shopify, and thousands more. The fingerprint database is compiled into the binary at build time, so there are no external lookups during detection.

Adding DNS-Based Detection

Header and HTML analysis misses an entire layer of infrastructure. CDN providers, hosting platforms, and email services leave fingerprints in DNS records—specifically in CNAME chains and NS records. Here’s a basic DNS detector:

package main

import (
    "fmt"
    "net"
    "strings"
)

type dnsSignature struct {
    suffix   string
    techName string
    category string
}

var signatures = []dnsSignature{
    {".cloudfront.net", "Amazon CloudFront", "CDN"},
    {".fastly.net", "Fastly", "CDN"},
    {".akamaiedge.net", "Akamai", "CDN"},
    {".netlify.app", "Netlify", "PaaS"},
    {".vercel-dns.com", "Vercel", "PaaS"},
    {".herokuapp.com", "Heroku", "PaaS"},
    {".fly.dev", "Fly.io", "PaaS"},
    {".github.io", "GitHub Pages", "PaaS"},
    {".azurefd.net", "Azure Front Door", "CDN"},
    {".b-cdn.net", "Bunny CDN", "CDN"},
}

func detectFromDNS(domain string) []string {
    cname, err := net.LookupCNAME(domain)
    if err != nil {
        return nil
    }
    cname = strings.ToLower(cname)

    var detected []string
    for _, sig := range signatures {
        if strings.HasSuffix(cname, sig.suffix) ||
            strings.HasSuffix(cname, sig.suffix+".") {
            detected = append(detected,
                fmt.Sprintf("%s (%s)", sig.techName, sig.category))
        }
    }
    return detected
}

func main() {
    domains := []string{"shopify.com", "netlify.com", "fly.io"}
    for _, d := range domains {
        techs := detectFromDNS(d)
        fmt.Printf("%s: %v\n", d, techs)
    }
}

This is a simplified version of what DetectZeStack does internally. The production system resolves the full CNAME chain (not just the first hop), checks NS records for providers like Cloudflare, inspects TLS certificate issuers, and matches against 111+ DNS signatures covering CDNs, PaaS, SaaS, email providers, and cloud infrastructure. For the full technical dive, see DNS-Based Technology Detection: Why Your CDN Can’t Hide.

What a DIY Approach Misses

Caching, Rate Limiting, and Edge Cases

A working prototype is straightforward. A production system is not. Here’s what you’ll need to handle beyond basic detection:

CPE Mapping and Vulnerability Correlation

Detecting that a site runs “Nginx 1.24” or “jQuery 3.6.0” is only half the story. Security teams need to know whether those versions have known vulnerabilities. This requires mapping each detected technology and version to a CPE (Common Platform Enumeration) identifier, then querying the National Vulnerability Database for matching CVEs.

Building and maintaining a CPE mapping table is a significant ongoing effort. Technology names in fingerprint databases don’t always match CPE vendor/product strings exactly. Version normalization is tricky. And the NVD API has its own rate limits and data formats to deal with. For more on how CPE identifiers work, see CPE Identifiers Explained for Security Teams.

Using the DetectZeStack API Instead

If you don’t want to build and maintain all of the above, you can call the DetectZeStack API from Go. It handles DNS resolution, TLS inspection, HTTP fingerprinting, CNAME chain resolution, caching, CPE mapping, and redirect following—and returns everything in a single JSON response.

Quick Test: The Demo Endpoint

Before writing any Go code, you can test the API from the command line. The /demo endpoint requires no API key (rate-limited to 20 requests per hour per IP):

$ curl -s "https://detectzestack.com/demo?url=github.com" | jq .
{
  "url": "https://github.com",
  "domain": "github.com",
  "technologies": [
    {
      "name": "GitHub Pages",
      "categories": ["PaaS"],
      "confidence": 80,
      "source": "dns"
    },
    {
      "name": "Ruby on Rails",
      "categories": ["Web frameworks"],
      "confidence": 100,
      "source": "headers"
    },
    {
      "name": "Varnish",
      "categories": ["Caching"],
      "confidence": 100,
      "source": "headers"
    }
  ],
  "meta": {
    "status_code": 200,
    "tech_count": 3,
    "scan_depth": "full"
  }
}

Each technology includes its name, categories, a confidence score, and the source that detected it (dns, tls, headers, or html). The source field is unique to DetectZeStack—it tells you exactly how each technology was identified.

Calling /analyze from Go with net/http

For production use, the authenticated /analyze endpoint goes through RapidAPI and counts against your plan quota. Here’s a complete, copy-pasteable Go program:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "time"
)

type Technology struct {
    Name       string   `json:"name"`
    Version    string   `json:"version,omitempty"`
    Categories []string `json:"categories"`
    Confidence int      `json:"confidence"`
    CPE        string   `json:"cpe,omitempty"`
    Source     string   `json:"source,omitempty"`
}

type DetectResult struct {
    URL          string       `json:"url"`
    Domain       string       `json:"domain"`
    Technologies []Technology `json:"technologies"`
    Meta         struct {
        StatusCode int    `json:"status_code"`
        TechCount  int    `json:"tech_count"`
        ScanDepth  string `json:"scan_depth"`
    } `json:"meta"`
}

func detect(domain, apiKey string) (*DetectResult, error) {
    endpoint := "https://detectzestack.p.rapidapi.com/analyze"
    req, err := http.NewRequest("GET", endpoint+"?url="+url.QueryEscape(domain), nil)
    if err != nil {
        return nil, err
    }
    req.Header.Set("X-RapidAPI-Key", apiKey)
    req.Header.Set("X-RapidAPI-Host", "detectzestack.p.rapidapi.com")

    client := &http.Client{Timeout: 30 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("API returned %d", resp.StatusCode)
    }

    var result DetectResult
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }
    return &result, nil
}

func main() {
    apiKey := os.Getenv("RAPIDAPI_KEY")
    if apiKey == "" {
        fmt.Fprintln(os.Stderr, "set RAPIDAPI_KEY environment variable")
        os.Exit(1)
    }

    result, err := detect("stripe.com", apiKey)
    if err != nil {
        fmt.Fprintln(os.Stderr, "error:", err)
        os.Exit(1)
    }

    fmt.Printf("Domain: %s (%d technologies)\n", result.Domain, result.Meta.TechCount)
    for _, t := range result.Technologies {
        fmt.Printf("  %-25s %-25s confidence=%d source=%s\n",
            t.Name, t.Categories[0], t.Confidence, t.Source)
    }
}

Run it with:

export RAPIDAPI_KEY="your_key_here"
go run main.go

Understanding the Response Format

The API response contains three main sections:

FieldTypeDescription
domain string The resolved domain (after redirects)
technologies[] array Each detected technology with name, categories, confidence, source, and optional CPE/version
meta.scan_depth string “full” when HTTP + DNS + TLS all succeeded; “partial” when HTTP was blocked but DNS/TLS still returned results
meta.tech_count int Total number of technologies detected
meta.status_code int HTTP status code from the target site

The source field on each technology is particularly useful for Go developers building detection pipelines. Technologies from dns and tls sources represent infrastructure-layer signals (CDN, hosting, certificate authority) that wappalyzergo alone cannot detect. Technologies from headers and html represent application-layer signals (frameworks, CMS, analytics) that overlap with what wappalyzergo provides.

Batch Analysis with /analyze/batch

For scanning multiple domains, use the /analyze/batch endpoint. It accepts up to 10 URLs per request and processes them server-side:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "time"
)

type BatchRequest struct {
    URLs []string `json:"urls"`
}

type BatchItem struct {
    URL          string `json:"url"`
    Domain       string `json:"domain"`
    Technologies []struct {
        Name       string   `json:"name"`
        Categories []string `json:"categories"`
        Source     string   `json:"source"`
    } `json:"technologies"`
    Error string `json:"error,omitempty"`
}

func main() {
    apiKey := os.Getenv("RAPIDAPI_KEY")

    payload, _ := json.Marshal(BatchRequest{
        URLs: []string{
            "stripe.com",
            "shopify.com",
            "github.com",
            "notion.so",
            "vercel.com",
        },
    })

    req, _ := http.NewRequest("POST",
        "https://detectzestack.p.rapidapi.com/analyze/batch",
        bytes.NewReader(payload))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-RapidAPI-Key", apiKey)
    req.Header.Set("X-RapidAPI-Host", "detectzestack.p.rapidapi.com")

    client := &http.Client{Timeout: 60 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    defer resp.Body.Close()

    var results []BatchItem
    json.NewDecoder(resp.Body).Decode(&results)

    for _, r := range results {
        if r.Error != "" {
            fmt.Printf("%s: ERROR %s\n", r.URL, r.Error)
            continue
        }
        fmt.Printf("%s: %d technologies\n", r.Domain, len(r.Technologies))
        for _, t := range r.Technologies {
            fmt.Printf("  - %s [%s]\n", t.Name, t.Source)
        }
    }
}

Each batch request counts as one API call per URL in the batch. For scanning hundreds of domains, send them in batches of 10 with a short delay between batches to stay within rate limits.

Comparing Build vs Buy for Go Developers

Here’s an honest comparison of the two approaches:

CapabilityDIY (wappalyzergo)DetectZeStack API
HTTP header detection Yes (7,300+ fingerprints) Yes
HTML/JS detection Yes Yes
DNS CNAME detection You build it Yes (111+ signatures)
TLS certificate inspection You build it Yes
CPE / CVE mapping You build it Yes
Redirect handling You build it Yes
Caching You build it Yes (server-side)
Partial results on failure You build it Yes (DNS/TLS when HTTP blocked)
External dependency None (compiled in) API call required
Cost Free (your compute) Free tier: 100 req/mo

Use wappalyzergo when you need offline detection, want zero external dependencies, or only care about HTTP-layer technologies (frameworks, CMS, analytics). It’s a solid library and it’s what DetectZeStack uses internally as one of its detection layers.

Use the API when you need the full picture—DNS, TLS, headers, HTML, CPE mapping, and caching—without spending weeks building infrastructure detection from scratch. The free tier gives you 100 requests per month to evaluate. Paid plans start at $9/month for 1,000 requests.

Hybrid approach: Some teams use wappalyzergo for real-time, in-process detection (e.g., inside a web crawler) and call the DetectZeStack API for deeper scans that need DNS/TLS data. The API’s source field makes it easy to see which technologies were found by which layer, so you can compare your local detection against the full result.

Getting Started with Your API Key

To start using the DetectZeStack API from Go:

  1. Sign up on RapidAPI and subscribe to the DetectZeStack free plan (100 requests/month, no credit card)
  2. Copy your API key from the RapidAPI dashboard
  3. Set it as an environment variable: export RAPIDAPI_KEY="your_key"
  4. Use the Go code examples above—they’re copy-pasteable and ready to run

If you’re building something more complex—a lead enrichment pipeline, a security audit tool, or a competitive intelligence dashboard—the companion Python tutorial covers CSV export and batch workflows that complement the Go examples here.

Related Reading

Start Detecting Tech Stacks with Go

100 requests/month free. No credit card required. DNS + TLS + HTTP detection in a single API call.

Get Your Free API Key

Get API updates and tech detection tips

Join the mailing list. No spam, unsubscribe anytime.