> ## Documentation Index
> Fetch the complete documentation index at: https://developers.soax.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Advanced: Multi-Geo Monitoring in Go

> Production-grade Go example that monitors a URL across multiple countries in parallel using SOAX residential proxies with a worker pool.

This example demonstrates a real-world pattern: checking the same URL from multiple countries in parallel, collecting structured results, and handling failures gracefully. It's designed as a starting point for price monitoring, ad verification, or geo-targeted data collection.

The program uses a worker pool where each worker gets its own session and country assignment. Workers retry on failure with exponential backoff, and the entire job can be cancelled with a context timeout.

## What this example covers

* Configurable worker pool with controlled concurrency
* One session per country (consistent IP per geo)
* Structured result collection across all workers
* Exponential backoff retry on failure
* Context-based timeout and graceful shutdown
* Clean separation between configuration, execution, and output

## Full code

```go theme={null}
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"net/http"
	"net/url"
	"os"
	"sync"
	"time"
)

// Config holds the job configuration.
type Config struct {
	PackageKey     string
	TargetURL      string
	Countries      []string
	MaxConcurrency int
	MaxRetries     int
	Timeout        time.Duration
}

// ProxyResult holds the outcome of a single geo-targeted request.
type ProxyResult struct {
	Country    string        `json:"country"`
	IP         string        `json:"ip,omitempty"`
	ISP        string        `json:"isp,omitempty"`
	City       string        `json:"city,omitempty"`
	StatusCode int           `json:"status_code"`
	BodySize   int           `json:"body_size"`
	Duration   time.Duration `json:"duration"`
	Attempts   int           `json:"attempts"`
	Error      string        `json:"error,omitempty"`
}

// CheckerResponse matches the SOAX checker JSON structure.
type CheckerResponse struct {
	Status bool `json:"status"`
	Data   struct {
		IP          string `json:"ip"`
		CountryCode string `json:"country_code"`
		CountryName string `json:"country_name"`
		Region      string `json:"region"`
		City        string `json:"city"`
		ISP         string `json:"isp"`
		Carrier     string `json:"carrier"`
	} `json:"data"`
}

func main() {
	cfg := Config{
		PackageKey: os.Getenv("SOAX_PACKAGE_KEY"),
		TargetURL:  "https://checker.soax.com/api/ipinfo",
		Countries: []string{
			"us", "gb", "de", "fr", "jp",
			"br", "au", "ca", "in", "kr",
		},
		MaxConcurrency: 5,
		MaxRetries:     3,
		Timeout:        30 * time.Second,
	}

	if cfg.PackageKey == "" {
		fmt.Println("Set SOAX_PACKAGE_KEY environment variable")
		os.Exit(1)
	}

	ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
	defer cancel()

	results := runJob(ctx, cfg)

	output, _ := json.MarshalIndent(results, "", "  ")
	fmt.Println(string(output))

	// Summary
	succeeded := 0
	for _, r := range results {
		if r.Error == "" {
			succeeded++
		}
	}
	fmt.Printf("\n%d/%d countries succeeded\n", succeeded, len(results))
}

// runJob dispatches workers and collects results.
func runJob(ctx context.Context, cfg Config) []ProxyResult {
	jobs := make(chan string, len(cfg.Countries))
	results := make(chan ProxyResult, len(cfg.Countries))

	// Start worker pool
	var wg sync.WaitGroup
	for i := 0; i < cfg.MaxConcurrency; i++ {
		wg.Add(1)
		go func(workerID int) {
			defer wg.Done()
			worker(ctx, workerID, cfg, jobs, results)
		}(i)
	}

	// Send jobs
	for _, country := range cfg.Countries {
		jobs <- country
	}
	close(jobs)

	// Wait for all workers to finish, then close results
	go func() {
		wg.Wait()
		close(results)
	}()

	// Collect results
	var collected []ProxyResult
	for r := range results {
		collected = append(collected, r)
	}

	return collected
}

// worker pulls countries from the jobs channel and fetches each one.
func worker(ctx context.Context, id int, cfg Config, jobs <-chan string, results chan<- ProxyResult) {
	for country := range jobs {
		select {
		case <-ctx.Done():
			results <- ProxyResult{
				Country: country,
				Error:   "cancelled: timeout exceeded",
			}
			return
		default:
			result := fetchWithRetry(ctx, cfg, country)
			results <- result
		}
	}
}

// fetchWithRetry attempts the request up to MaxRetries with exponential backoff.
func fetchWithRetry(ctx context.Context, cfg Config, country string) ProxyResult {
	var lastErr error

	for attempt := 1; attempt <= cfg.MaxRetries; attempt++ {
		result, err := fetchCountry(ctx, cfg, country)
		if err == nil {
			result.Attempts = attempt
			return result
		}

		lastErr = err

		// Don't sleep after the last attempt
		if attempt < cfg.MaxRetries {
			backoff := time.Duration(math.Pow(2, float64(attempt-1))) * 500 * time.Millisecond
			select {
			case <-ctx.Done():
				return ProxyResult{
					Country:  country,
					Attempts: attempt,
					Error:    "cancelled: timeout exceeded",
				}
			case <-time.After(backoff):
				// continue to next attempt
			}
		}
	}

	return ProxyResult{
		Country:  country,
		Attempts: cfg.MaxRetries,
		Error:    lastErr.Error(),
	}
}

// fetchCountry makes a single proxied request for a given country.
func fetchCountry(ctx context.Context, cfg Config, country string) (ProxyResult, error) {
	sessionID := fmt.Sprintf("geo_%s", country)
	proxyStr := fmt.Sprintf("http://country-%s-session-%s:%s@proxy.soax.com:1337", country, sessionID, cfg.PackageKey)

	proxyURL, err := url.Parse(proxyStr)
	if err != nil {
		return ProxyResult{}, fmt.Errorf("invalid proxy URL: %w", err)
	}

	client := &http.Client{
		Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
		Timeout:   10 * time.Second,
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, cfg.TargetURL, nil)
	if err != nil {
		return ProxyResult{}, fmt.Errorf("request creation failed: %w", err)
	}

	start := time.Now()
	resp, err := client.Do(req)
	duration := time.Since(start)

	if err != nil {
		return ProxyResult{}, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return ProxyResult{}, fmt.Errorf("body read failed: %w", err)
	}

	result := ProxyResult{
		Country:    country,
		StatusCode: resp.StatusCode,
		BodySize:   len(body),
		Duration:   duration,
	}

	// Parse checker response to extract IP metadata
	var checker CheckerResponse
	if json.Unmarshal(body, &checker) == nil && checker.Status {
		result.IP = checker.Data.IP
		result.ISP = checker.Data.ISP
		result.City = checker.Data.City
	}

	return result, nil
}
```

## How to run it

Set your package key as an environment variable:

```bash theme={null}
export SOAX_PACKAGE_KEY="abc123"
go run main.go
```

## Example output

```json theme={null}
[
  {
    "country": "us",
    "ip": "104.28.55.12",
    "isp": "Comcast",
    "city": "Denver",
    "status_code": 200,
    "body_size": 203,
    "duration": 842000000,
    "attempts": 1
  },
  {
    "country": "gb",
    "ip": "86.12.145.78",
    "isp": "BT",
    "city": "Manchester",
    "status_code": 200,
    "body_size": 198,
    "duration": 1103000000,
    "attempts": 1
  },
  {
    "country": "de",
    "ip": "91.64.33.210",
    "isp": "Deutsche Telekom",
    "city": "Berlin",
    "status_code": 200,
    "body_size": 201,
    "duration": 967000000,
    "attempts": 1
  }
]

10/10 countries succeeded
```

## How it works

**Worker pool.** The `MaxConcurrency` setting controls how many countries are checked at the same time. With 10 countries and 5 workers, the job runs in two waves. Increase concurrency to run all countries simultaneously, or decrease it to stay within your package's connection limits.

**One session per country.** Each country gets a session ID (`geo_us`, `geo_gb`, etc.). This means the same IP is reused if you run the job multiple times within the session's 60-second inactivity window. For monitoring jobs that run on a schedule, this gives you consistent IPs across runs.

**Exponential backoff.** If a request fails, the worker waits before retrying: 500ms after the first failure, 1s after the second, 2s after the third. This avoids hammering the proxy during transient issues.

**Context timeout.** The entire job has a 30-second deadline. If any worker is still running when the timeout hits, it gets cancelled and returns a clear error. This prevents hung requests from blocking your pipeline.

**Structured results.** Every result includes the country, exit IP, ISP, response timing, retry count, and any errors. This makes it easy to pipe into a database, alerting system, or dashboard.

## Adapting this for your use case

**Change the target URL.** Replace `checker.soax.com/api/ipinfo` with the URL you're monitoring. The result struct will need updating to match your target's response format.

**Add more countries.** The `Countries` slice controls which geos are checked. Add or remove country codes as needed.

**Switch to mobile.** Change the proxy string to include `network-mob`:

```go theme={null}
proxyStr := fmt.Sprintf("http://network-mob-country-%s-session-%s:%s@proxy.soax.com:1337", country, sessionID, cfg.PackageKey)
```

**Add rotation.** If you want a fresh IP on every run instead of reusing sessions, remove the `session` parameter from the proxy string, or append a timestamp to the session ID to force a new session each time:

```go theme={null}
sessionID := fmt.Sprintf("geo_%s_%d", country, time.Now().Unix())
```

**Write results to a file.** Replace the `fmt.Println` at the end with a file write for scheduled jobs:

```go theme={null}
os.WriteFile("results.json", output, 0644)
```

## Next steps

<CardGroup cols={2}>
  <Card title="Residential proxies" icon="wifi" href="/proxies/residential">
    Full parameter reference for session, rotation, and error handling options.
  </Card>

  <Card title="Go examples" icon="code" href="/examples/go">
    Simpler Go examples for getting started.
  </Card>

  <Card title="Error codes" icon="triangle-exclamation" href="/troubleshooting/error-codes">
    Understand what errors mean and how to handle them in your code.
  </Card>

  <Card title="Connection debugging" icon="wrench" href="/troubleshooting/connection-debugging">
    Troubleshoot timeouts, auth failures, and geo mismatches.
  </Card>
</CardGroup>
