Tech Stack Detection in Go: Build or Buy Guide
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:
- Resolve DNS records (CNAME chains, NS records, MX records)
- Fetch the HTTP response (headers, HTML body, status code)
- Inspect the TLS certificate (issuer, subject, SANs)
- 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:
- Redirect chains — Many domains redirect (HTTP to HTTPS, www to apex, old domain to new). You need to follow the full chain and detect on the final destination while caching against the resolved domain, not the input URL.
- Caching — Tech stacks don’t change every minute. Without caching, you’ll hammer the same domains repeatedly and waste time waiting for DNS and HTTP responses you already have.
- Rate limiting — If you’re scanning at scale, you need to respect both your own infrastructure limits and the target sites’ rate limits. Getting IP-banned mid-scan is a real problem.
- SSRF protection — If your scanner accepts user-supplied URLs, you must block requests to internal IPs (127.0.0.1, 10.x.x.x, 169.254.x.x) to prevent server-side request forgery attacks.
- Timeouts and partial results — Some sites block automated requests, return CAPTCHAs, or time out. A production system needs to return partial results (DNS and TLS data) even when the HTTP fetch fails.
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:
| Field | Type | Description |
|---|---|---|
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:
| Capability | DIY (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:
- Sign up on RapidAPI and subscribe to the DetectZeStack free plan (100 requests/month, no credit card)
- Copy your API key from the RapidAPI dashboard
- Set it as an environment variable:
export RAPIDAPI_KEY="your_key" - 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
- DNS-Based Technology Detection: Why Your CDN Can’t Hide — Technical deep dive on CNAME chain resolution and 111+ DNS signatures
- Detect Vulnerable Technologies with CPE — How CPE identifiers map detected tech to known CVEs
- Tech Stack Detection with Python (Under 50 Lines) — Companion tutorial with batch scanning and CSV export
- Detect Any Website’s Tech Stack With a Single API Call — API overview covering all four detection layers
- Website Technology Checker API — Full API reference and endpoint documentation
- How to Detect the CDN and Hosting Provider of Any Website — DNS, headers, TLS, and IP range methods explained
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