projectdiscovery/subfinder · warning

virustotal quota exhausted (HTTP 429); some subdomains for

Error message

virustotal quota exhausted (HTTP 429); some subdomains for %s may be missing

What it means

The virustotal source replaces the generic status-code error with this message when the API returns HTTP 429. VirusTotal's free tier allows 500 requests/day; once exhausted every call returns 429 and some subdomains may be missing. The library raises this so operators know to switch to an enterprise key or reduce request scope (issue #1718).

Solutions

  1. Supply a VirusTotal Premium/enterprise API key with higher quotas.
  2. Reduce scope: enumerate fewer domains per day or use narrower queries to stay under 500 requests/day.
  3. Wait for the daily quota window to reset before re-running the enumeration.

Example fix

// before: free key exhausted mid-run
export VIRUSTOTAL_API_KEY=free_key
// after: use an enterprise key or spread queries across days
export VIRUSTOTAL_API_KEY=<enterprise_key>
Defensive patterns

Strategy: validation

Validate before calling

// check remaining quota cheaply before a run
resp, err := http.Get("https://www.virustotal.com/api/v3/domains/example.com" + "?limit=1") // with key header
if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
  log.Fatal("VirusTotal quota exhausted; use enterprise key or wait for reset")
}

Try / catch

for r := range results {
  if r.Type == subscraping.Error && strings.Contains(r.Error.Error(), "quota exhausted") {
    // stop VT-dependent work; switch key or schedule retry after reset
  }
}

Prevention

When it happens

Trigger: Enumerating a domain when the VirusTotal API key's daily (or per-minute) quota is already consumed, so the request gets http.StatusTooManyRequests (429).

Common situations: Running broad enumerations with a free-tier VT key across many domains in one day; multiple tools sharing one free key; retry loops amplifying quota consumption.

Understand the failure class

Related errors


AI-assisted analysis of projectdiscovery/subfinder@7a0b91f0fa (2026-09-06). Data as JSON: /api/errors/cd910bd4284a0dfd. Report an issue: GitHub.

Appendix: source

Thrown at pkg/subscraping/sources/virustotal/virustotal.go:78

		for {
			select {
			case <-ctx.Done():
				return
			default:
			}
			var url = fmt.Sprintf("https://www.virustotal.com/api/v3/domains/%s/subdomains?limit=40", domain)
			if cursor != "" {
				url = fmt.Sprintf("%s&cursor=%s", url, cursor)
			}
			s.requests++
			resp, err := session.Get(ctx, url, "", map[string]string{"x-apikey": randomApiKey})
			if err != nil {
				// The free tier grants 500 requests/day; once it is exhausted every
				// call returns HTTP 429. Surface an actionable message instead of the
				// generic "unexpected status code 429" so operators know to supply an
				// enterprise key or lower the scope (see #1718).
				if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
					err = fmt.Errorf("virustotal quota exhausted (HTTP 429); some subdomains for %s may be missing", domain)
				}
				results <- subscraping.Result{Source: s.Name(), Type: subscraping.Error, Error: err}
				s.errors++
				session.DiscardHTTPResponse(resp)
				return
			}

			var data response
			err = jsoniter.NewDecoder(resp.Body).Decode(&data)
			// Close the body per iteration; deferring inside the loop would keep
			// every page's body (and its connection) open until the goroutine exits.
			if closeErr := resp.Body.Close(); closeErr != nil {
				results <- subscraping.Result{Source: s.Name(), Type: subscraping.Error, Error: closeErr}
				s.errors++
			}
			if err != nil {
				results <- subscraping.Result{Source: s.Name(), Type: subscraping.Error, Error: err}
				s.errors++

View on GitHub (pinned to 7a0b91f0fa)