Email Verification in Go: Building a Concurrent Validation Service with Goroutines

Key Takeaways

  • Go's goroutines and channels make it ideal for building high-throughput email verification pipelines that process thousands of addresses concurrently.
  • EmailVerifierAPI's v2 endpoint returns 16 data points per address including status, disposable detection, SMTP verification, and sub-status codes.
  • A worker pool pattern with 10 goroutines can verify approximately 2,400 emails per minute with proper rate limiting.
  • Production implementations should include context-based timeouts, retry logic for transient errors, and structured result aggregation.

Go's concurrency model is a natural fit for email verification in Go workflows. Where other languages require thread pools or async/await patterns, Go gives you goroutines and channels as first-class primitives. This makes building a concurrent verification pipeline straightforward and efficient.

In this guide, you will integrate EmailVerifierAPI's v2 endpoint into a Go application, build a worker pool for batch verification, and handle the full response payload including status, disposable detection, and SMTP sub-status codes.

Single Email Verification: The Foundation

Start with a basic verification function that calls the EmailVerifierAPI v2 endpoint for a single address. Here is the curl equivalent, followed by the Go implementation.

curl -X GET "https://emailverifierapi.com/v2/verify?email=test@example.com" 
  -H "x-api-key: YOUR_API_KEY"

The response includes the full verification payload:

{
  "status": "passed",
  "isDisposable": false,
  "isFreeService": true,
  "isOffensive": false,
  "isRoleAccount": false,
  "isGibberish": false,
  "smtp_check": "success",
  "sub_status": "mailboxExists"
}

Now here is the Go implementation with proper struct definitions and error handling:

package main

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

type VerifyResult struct {
    Status        string `json:"status"`
    IsDisposable  bool   `json:"isDisposable"`
    IsFreeService bool   `json:"isFreeService"`
    IsOffensive   bool   `json:"isOffensive"`
    IsRoleAccount bool   `json:"isRoleAccount"`
    IsGibberish   bool   `json:"isGibberish"`
    SMTPCheck     string `json:"smtp_check"`
    SubStatus     string `json:"sub_status"`
}

func verifyEmail(ctx context.Context, apiKey, email string) (*VerifyResult, error) {
    endpoint := fmt.Sprintf(
        "https://emailverifierapi.com/v2/verify?email=%s",
        url.QueryEscape(email),
    )

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }
    req.Header.Set("x-api-key", apiKey)

    client := &http.Client{Timeout: 15 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

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

    var result VerifyResult
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("decoding response: %w", err)
    }
    return &result, nil
}

The VerifyResult struct maps directly to the API response fields. The context.Context parameter enables timeout control and cancellation, which is essential for production use. For full endpoint documentation, see the Go integration guide.

Concurrent Batch Verification with a Worker Pool

Single-address verification is useful for real-time form validation, but batch verification requires concurrency. The worker pool pattern uses a fixed number of goroutines to process a channel of email addresses in parallel.

type BatchResult struct {
    Email  string
    Result *VerifyResult
    Err    error
}

func verifyBatch(ctx context.Context, apiKey string, emails []string, workers int) []BatchResult {
    jobs := make(chan string, len(emails))
    results := make(chan BatchResult, len(emails))

    // Start worker goroutines
    for w := 0; w < workers; w++ {
        go func() {
            for email := range jobs {
                res, err := verifyEmail(ctx, apiKey, email)
                results <- BatchResult{Email: email, Result: res, Err: err}
            }
        }()
    }

    // Send jobs
    for _, email := range emails {
        jobs <- email
    }
    close(jobs)

    // Collect results
    output := make([]BatchResult, 0, len(emails))
    for i := 0; i < len(emails); i++ {
        output = append(output, <-results)
    }
    return output
}
???? Pro Tip Start with 10 worker goroutines and adjust based on your API plan's rate limits. Too many concurrent workers can trigger rate limiting, while too few leave throughput on the table. Monitor your credit consumption dashboard to find the optimal concurrency level.

Interpreting Verification Results

The status field returns one of four values, and your application logic should branch accordingly:

  • passed: The address is valid and deliverable. Safe to send.
  • failed: The address is invalid. Suppress immediately. Check sub_status for the specific reason (mailboxDoesNotExist, domainDoesNotExist, invalidSyntax).
  • unknown: Verification was inconclusive, often due to greylisting or an unresponsive server. Retry later or apply conservative sending rules.
  • transient: A temporary error prevented verification. Queue for retry.

The boolean flags (isDisposable, isRoleAccount, isGibberish) provide additional filtering dimensions. For lead generation use cases, you may want to suppress disposable and role accounts even when the status is passed.

Go's net/http client supports connection pooling by default, making it efficient for high-volume API calls without additional configuration. Built-in transport reuse eliminates connection overhead for batch verification workloads

Production Considerations

Timeouts and cancellation. Always pass a context with a deadline to verifyEmail. For batch jobs, use context.WithTimeout to set a global deadline that prevents runaway operations. A 30-second timeout per verification is a reasonable default for most use cases.

Retry logic. Wrap the HTTP call in a retry loop for transient results and 5xx server errors. Implement exponential backoff with jitter to avoid thundering herd problems against the API. A maximum of 3 retries with 1-second, 2-second, and 4-second delays covers most transient failures without excessive wait times.

Result persistence. Write batch results to a database or CSV file as they arrive rather than holding everything in memory. For large lists (100K+ addresses), streaming results to disk prevents memory pressure.

Graceful shutdown. In production services, handle OS signals (SIGINT, SIGTERM) by canceling the context passed to your worker pool. This ensures in-flight verifications complete cleanly and results are persisted before the process exits. Go's signal.NotifyContext makes this straightforward.

Filtering Beyond Pass/Fail

The binary passed/failed check is the starting point, but production systems should apply layered filtering based on the full response payload. For B2B lead pipelines, suppress addresses where isRoleAccount is true, since role addresses (info@, support@, sales@) rarely represent individual decision-makers.

For SaaS signup flows, flag addresses where isDisposable is true. Disposable email users often sign up for free trials with no intent to convert. Filtering these at registration reduces trial abuse and improves your conversion funnel accuracy.

The isFreeService flag is useful for B2B segmentation. While free email providers like Gmail are perfectly valid for individual users, a high concentration of free service addresses in a B2B import may indicate low-quality lead data that warrants closer inspection. Use this flag for scoring and prioritization, not blanket suppression.

The full EmailVerifierAPI integrations hub includes language-specific guides for additional implementation patterns and edge cases.

Frequently Asked Questions

How many concurrent requests can I make to EmailVerifierAPI?

Concurrency limits depend on your plan tier. Start with 10 concurrent goroutines for standard plans and scale up based on your rate limit headers. The API returns standard rate limit headers that your Go client can read to dynamically adjust concurrency.

Should I verify emails synchronously on form submission or in batches?

Use both. Synchronous single-address verification on form submission provides instant feedback to users and prevents bad data from entering your system. Batch verification handles bulk imports and periodic list cleaning. The same Go function works for both patterns.

What does the sub_status field tell me that status does not?

The status field gives you the verdict (passed, failed, unknown, transient), while sub_status explains why. For example, a failed status could mean mailboxDoesNotExist, domainDoesNotExist, or invalidSyntax. Each sub-status suggests a different remediation action and helps you classify the type of data quality issue in your pipeline.