Repository: githubexporter/github-exporter

Issue:
Metrics collections fail after upgrade
After upgrading to v2.0.2 I'm no longer able to see metrics for one org. Another org works. [/+]

Environment variables for non-working org:

```shell
 LOG_LEVEL = "debug"
 LISTEN_PORT = "9171"
 GITHUB_RATE_LIMIT = 20000
 GITHUB_APP = true
 GITHUB_APP_ID = 123456
 GITHUB_APP_INSTALLATION_ID = 08765309
 GITHUB_APP_KEY_PATH = "/secrets/key.pem"
 ORGS = "MyFirstOrg"
```

Working org environment variables: 

```shell
 LOG_LEVEL = "debug"
 LISTEN_PORT = "9172"
 GITHUB_RATE_LIMIT = 20000
 GITHUB_APP = true
 GITHUB_APP_ID = 123457
 GITHUB_APP_INSTALLATION_ID = 18765309
 GITHUB_APP_KEY_PATH = "/secrets/key.pem"
 ORGS = "MySecondOrg"
```

Logs from both orgs say:

```shell
time="2025-10-22T17:10:03Z" level=info msg="Starting Exporter"
time="2025-10-22T17:13:09Z" level=info msg="collecting metrics"
time="2025-10-22T17:18:09Z" level=info msg="collecting metrics"
```

The working org does show the new `resource` labels for `github_rate_limit` metrics:

`{__name__="github_rate_limit", instance="10.51.156.118:9172", job="foo", org="MySecondOrg", resource="actions_runner_registration"}`

The working org only has ~20 repos, while the non-working one has over 200.

What could I try to get this working again?

**Update / New Findings:**

After further investigation, I've identified a likely cause for the regression after upgrading:

**Previous versions of the exporter used a `GITHUB_RATE_LIMIT` environment variable.** This setting allowed the exporter to proactively refresh the GitHub App token if the remaining API rate limit dropped below the configured threshold (default 15,000). This logic ensured that for organizations with large numbers of repositories or high API usage, the exporter could avoid exhausting the GitHub API quota by generating a new token before hitting the rate limit.

**This logic was removed in [commit ce81cac6997e866824714472fa9a893140f4951e](https://github.com/githubexporter/github-exporter/commit/ce81cac6997e866824714472fa9a893140f4951e).** The exporter now only refreshes tokens on expiry, not on low quota, relying on the standard go-github library behavior.

**Impact:**
- For large organizations or those with high-frequency scraping, the exporter can now hit the GitHub API rate limit and stop collecting metrics until the quota resets, since it no longer refreshes tokens proactively.
- This explains why after upgrading, metrics collection fails for large orgs but continues to work for smaller orgs.

**Request:**
Would it be possible to restore the configuration and logic that allowed quota-based token refreshing (using `GITHUB_RATE_LIMIT` or similar) in the current version? This is important for supporting metrics collection in environments with high API usage, where hitting the rate limit is otherwise unavoidable.

Thank you for looking into this!

Hints from issue comments:
@chewrocca: Tried removing GITHUB_RATE_LIMIT, tried setting GITHUB_RESULTS_PER_PAGE to a lower value like 30, and tried limiting to specific repos using the REPOS env var.  Nothing seemed to help.  @henrymcconville: Hi @chewrocca, thanks for the report. It looks like in the new version I inadvertently fixed an issue with fetching repo release metrics which caused the runtime to balloon significantly.

I've added a couple of fixes to reduce the run time:
* Request concurrency for each type of request. These default to `1` so you'll probably want to play around for what works best for you
* Ability to disable Repo Releases metrics if you don't need them

See README for the environment variable names for the above metrics all prefixed with `FETCH_`.

This has had roughly the following impact on runtime for an org with 153 repositories:
```
v2.0.1 - 3m20.9s
v2.1.0 - concurrency 1, releases disabled - 1m40.1s
v2.1.0 - concurrency 50 - releases disabled - 13.4s
v2.1.0 - concurrency 50 - releases enabled - 14.1s
```

This is available now as release [v2.1.0](https://github.com/githubexporter/github-exporter/releases/tag/v2.1.0)

Feel free to shout if you have any more issues@henrymcconville: Hey sorry I missed your update. I've just raised https://github.com/githubexporter/github-exporter/issues/129 with the extra info so I'll try look into that over the next couple of days@chewrocca: Tried out v2.1.0 

```shell
level=info msg="Starting Exporter"
level=info msg="collecting metrics"
level=debug msg="Fetching data for organisation: SiliconLabsInternal"
level=debug msg="Fetching releases for repository: MyOrg/repo1"
level=debug msg="Fetching pull requests for repository: MyOrg/repo1"
level=debug msg="Fetching releases for repository: MyOrg/repo2"
level=debug msg="Fetching pull requests for repository: MyOrg/repo2"
.....repeats for MANY repos in the org...
level=debug msg="fetching rate limits"
.....next scrape interval repeats........
```

With `FETCH_REPO_RELEASES_ENABLED = false`

```shell
level=info msg="Starting Exporter"
level=info msg="collecting metrics"
level=debug msg="Fetching pull requests for repository: MyOrg/repo1"
level=debug msg="Fetching pull requests for repository: MyOrg/repo2"
.....repeats for MANY repos in the org...
level=debug msg="fetching rate limits"
.....next scrape interval repeats........
```

I've tried `FETCH_ORGS_CONCURRENCY` values of 1, 25, 50, 75, 100, 1000, 20000.  

Unfortunately, I'm still not seeing metrics for the orgs with +200 repos.    @henrymcconville: Hmm that's weird. I've just tried with the following settings and it took 13.7 seconds against the `alibaba` org with 474 repos:

```
version: "2"

services:
  github-exporter-github-app:
    tty: true
    stdin_open: true
    expose:
      - 9171
    ports:
      - 9171:9171
    image: githubexporter/github-exporter:v2.1.0
    environment:
      - LOG_LEVEL=debug
      - ORGS=alibaba
      - LISTEN_PORT=9171
      - GITHUB_APP=true
      - GITHUB_APP_ID=xxxxxx
      - GITHUB_APP_INSTALLATION_ID=xxxxxx
      - GITHUB_APP_KEY_PATH=/key.pem
      - FETCH_ORGS_CONCURRENCY=50
      - FETCH_ORG_REPOS_CONCURRENCY=50
      - FETCH_REPO_RELEASES_ENABLED=false
    restart: unless-stopped
    volumes:
      - "./key.pem:/key.pem:ro"
```@chewrocca: Using this combination worked.  I'm now seeing metrics.  Thank you.  

```shell
        FETCH_ORGS_CONCURRENCY      = 50
        FETCH_ORG_REPOS_CONCURRENCY = 50
        FETCH_REPO_RELEASES_ENABLED = true
```

`FETCH_REPO_RELEASES_ENABLED = false` also worked.  

Solution patch:
diff --git a/README.md b/README.md
index 4e2d9d4..7e3a4ef 100644
--- a/README.md
+++ b/README.md
@@ -6,23 +6,28 @@ Exposes basic metrics for your repositories from the GitHub API, to a Prometheus
 
 This exporter is configured via environment variables. All variables are optional unless otherwise stated. Below is a list of supported configuration values:
 
-| Variable                     | Description                                                                        | Default                  |
-|------------------------------|------------------------------------------------------------------------------------|--------------------------|
-| `ORGS`                       | Comma-separated list of GitHub organizations to monitor (e.g. `org1,org2`).        |                          |
-| `REPOS`                      | Comma-separated list of repositories to monitor (e.g. `user/repo1,user/repo2`).    |                          |
-| `USERS`                      | Comma-separated list of GitHub users to monitor (e.g. `user1,user2`).              |                          |
-| `GITHUB_TOKEN`               | GitHub personal access token for API authentication.                               |                          |
-| `GITHUB_TOKEN_FILE`          | Path to a file containing a GitHub personal access token.                          |                          |
-| `GITHUB_APP`                 | Set to `true` to authenticate as a GitHub App.                                     | `false`                  |
-| `GITHUB_APP_ID`              | The App ID of the GitHub App. Required if `GITHUB_APP` is `true`.                  |                          |
-| `GITHUB_APP_INSTALLATION_ID` | The Installation ID of the GitHub App. Required if `GITHUB_APP` is `true`.         |                          |
-| `GITHUB_APP_KEY_PATH`        | Path to the GitHub App private key file. Required if `GITHUB_APP` is `true`.       |                          |
-| `GITHUB_RATE_LIMIT_ENABLED`  | Whether to fetch GitHub API rate limit metrics (`true` or `false`).                | `true`                   |
-| `GITHUB_RESULTS_PER_PAGE`    | Number of results to request per page from the GitHub API (max 100).               | `100`                    |
-| `API_URL`                    | GitHub API URL. You should not need to change this unless using GitHub Enterprise. | `https://api.github.com` |
-| `LISTEN_PORT`                | The port the exporter will listen on.                                              | `9171`                   |
-| `METRICS_PATH`               | The HTTP path to expose Prometheus metrics.                                        | `/metrics`               |
-| `LOG_LEVEL`                  | Logging level (`debug`, `info`, `warn`, `error`).                                  | `info`                   |
+| Variable                      | Description                                                                        | Default                  |
+|-------------------------------|------------------------------------------------------------------------------------|--------------------------|
+| `ORGS`                        | Comma-separated list of GitHub organizations to monitor (e.g. `org1,org2`).        |                          |
+| `REPOS`                       | Comma-separated list of repositories to monitor (e.g. `user/repo1,user/repo2`).    |                          |
+| `USERS`                       | Comma-separated list of GitHub users to monitor (e.g. `user1,user2`).              |                          |
+| `GITHUB_TOKEN`                | GitHub personal access token for API authentication.                               |                          |
+| `GITHUB_TOKEN_FILE`           | Path to a file containing a GitHub personal access token.                          |                          |
+| `GITHUB_APP`                  | Set to `true` to authenticate as a GitHub App.                                     | `false`                  |
+| `GITHUB_APP_ID`               | The App ID of the GitHub App. Required if `GITHUB_APP` is `true`.                  |                          |
+| `GITHUB_APP_INSTALLATION_ID`  | The Installation ID of the GitHub App. Required if `GITHUB_APP` is `true`.         |                          |
+| `GITHUB_APP_KEY_PATH`         | Path to the GitHub App private key file. Required if `GITHUB_APP` is `true`.       |                          |
+| `GITHUB_RATE_LIMIT_ENABLED`   | Whether to fetch GitHub API rate limit metrics (`true` or `false`).                | `true`                   |
+| `GITHUB_RESULTS_PER_PAGE`     | Number of results to request per page from the GitHub API (max 100).               | `100`                    |
+| `FETCH_REPO_RELEASES_ENABLED` | Whether to fetch repository release metrics (`true` or `false`).                   | `true`                   |
+| `FETCH_ORGS_CONCURRENCY`      | Number of concurrent requests to make when fetching organization data.             | `1`                      |
+| `FETCH_ORG_REPOS_CONCURRENCY` | Number of concurrent requests to make when fetching organization repository data.  | `1`                      |
+| `FETCH_USERS_CONCURRENCY`     | Number of concurrent requests to make when fetching user data.                     | `1`                      |
+| `FETCH_USERS_CONCURRENCY`     | Number of concurrent requests to make when fetching repository data.               | `1`                      |
+| `API_URL`                     | GitHub API URL. You should not need to change this unless using GitHub Enterprise. | `https://api.github.com` |
+| `LISTEN_PORT`                 | The port the exporter will listen on.                                              | `9171`                   |
+| `METRICS_PATH`                | The HTTP path to expose Prometheus metrics.                                        | `/metrics`               |
+| `LOG_LEVEL`                   | Logging level (`debug`, `info`, `warn`, `error`).                                  | `info`                   |
 
 ### Credential Precedence
 
diff --git a/VERSION b/VERSION
index b02d37b..1defe53 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-v2.0.2
\ No newline at end of file
+v2.1.0
diff --git a/config/config.go b/config/config.go
index 08050d2..7fa5cb0 100644
--- a/config/config.go
+++ b/config/config.go
@@ -16,19 +16,24 @@ import (
 
 // Config struct holds runtime configuration required for the application
 type Config struct {
-	MetricsPath            string   `envconfig:"METRICS_PATH" required:"false" default:"/metrics"`
-	ListenPort             string   `envconfig:"LISTEN_PORT" required:"false" default:"9171"`
-	LogLevel               string   `envconfig:"LOG_LEVEL" required:"false" default:"INFO"`
-	ApiUrl                 *url.URL `envconfig:"API_URL" required:"false" default:"https://api.github.com"`
-	Repositories           []string `envconfig:"REPOS" required:"false"`
-	Organisations          []string `envconfig:"ORGS" required:"false"`
-	Users                  []string `envconfig:"USERS" required:"false"`
-	GitHubResultsPerPage   int      `envconfig:"GITHUB_RESULTS_PER_PAGE" required:"false" default:"100"`
-	GithubToken            string   `envconfig:"GITHUB_TOKEN" required:"false"`
-	GithubTokenFile        string   `envconfig:"GITHUB_TOKEN_FILE" required:"false"`
-	GitHubApp              bool     `envconfig:"GITHUB_APP" required:"false" default:"false"`
-	GitHubRateLimitEnabled bool     `envconfig:"GITHUB_RATE_LIMIT_ENABLED" required:"false" default:"true"`
-	*GitHubAppConfig       `ignored:"true"`
+	MetricsPath              string   `envconfig:"METRICS_PATH" required:"false" default:"/metrics"`
+	ListenPort               string   `envconfig:"LISTEN_PORT" required:"false" default:"9171"`
+	LogLevel                 string   `envconfig:"LOG_LEVEL" required:"false" default:"INFO"`
+	ApiUrl                   *url.URL `envconfig:"API_URL" required:"false" default:"https://api.github.com"`
+	Repositories             []string `envconfig:"REPOS" required:"false"`
+	Organisations            []string `envconfig:"ORGS" required:"false"`
+	Users                    []string `envconfig:"USERS" required:"false"`
+	GitHubResultsPerPage     int      `envconfig:"GITHUB_RESULTS_PER_PAGE" required:"false" default:"100"`
+	GithubToken              string   `envconfig:"GITHUB_TOKEN" required:"false"`
+	GithubTokenFile          string   `envconfig:"GITHUB_TOKEN_FILE" required:"false"`
+	GitHubApp                bool     `envconfig:"GITHUB_APP" required:"false" default:"false"`
+	GitHubRateLimitEnabled   bool     `envconfig:"GITHUB_RATE_LIMIT_ENABLED" required:"false" default:"true"`
+	FetchRepoReleasesEnabled bool     `envconfig:"FETCH_REPO_RELEASES_ENABLED" required:"false" default:"true"`
+	FetchOrgsConcurrency     int      `envconfig:"FETCH_ORGS_CONCURRENCY" required:"false" default:"1"`
+	FetchOrgReposConcurrency int      `envconfig:"FETCH_ORG_REPOS_CONCURRENCY" required:"false" default:"1"`
+	FetchReposConcurrency    int      `envconfig:"FETCH_REPOS_CONCURRENCY" required:"false" default:"1"`
+	FetchUsersConcurrency    int      `envconfig:"FETCH_USERS_CONCURRENCY" required:"false" default:"1"`
+	*GitHubAppConfig         `ignored:"true"`
 }
 
 type GitHubAppConfig struct {
diff --git a/exporter/prometheus.go b/exporter/prometheus.go
index a124775..d0229c4 100644
--- a/exporter/prometheus.go
+++ b/exporter/prometheus.go
@@ -9,6 +9,7 @@ import (
 	"github.com/google/go-github/v76/github"
 	"github.com/prometheus/client_golang/prometheus"
 	log "github.com/sirupsen/logrus"
+	"golang.org/x/sync/errgroup"
 )
 
 // Describe - loops through the API metrics and passes them to prometheus.Describe
@@ -68,6 +69,7 @@ func (e *Exporter) getRateLimits(ctx context.Context) (*[]RateLimit, error) {
 		return nil, nil
 	}
 
+	log.Debug("fetching rate limits")
 	rates, _, err := e.Client.RateLimit.Get(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("fetching rate limits: %w", err)
@@ -108,27 +110,37 @@ func (e *Exporter) getRateLimits(ctx context.Context) (*[]RateLimit, error) {
 // getRepoMetrics fetches metrics for the configured repositories
 func (e *Exporter) getRepoMetrics(ctx context.Context) ([]*Datum, error) {
 	var data []*Datum
+
+	gRepos, rCtx := errgroup.WithContext(ctx)
+	gRepos.SetLimit(e.FetchReposConcurrency)
+
 	for _, m := range e.Repositories {
-		// Split the repository string into owner and name
-		parts := strings.Split(m, "/")
-		if len(parts) != 2 {
-			log.Errorf("Invalid repository format: %s", m)
-			continue
-		}
+		gRepos.Go(func() error {
+			// Split the repository string into owner and name
+			parts := strings.Split(m, "/")
+			if len(parts) != 2 {
+				return fmt.Errorf("invalid repository format: %s", m)
+			}
 
-		repo, _, err := e.Client.Repositories.Get(ctx, parts[0], parts[1])
-		if err != nil {
-			log.Errorf("Error fetching repository data: %v", err)
-			continue
-		}
+			log.Debugf("Fetching data for repository: %s", m)
+			repo, _, err := e.Client.Repositories.Get(rCtx, parts[0], parts[1])
+			if err != nil {
+				return fmt.Errorf("fetching repository data: %v", err)
+			}
 
-		d, err := e.parseRepo(ctx, *repo)
-		if err != nil {
-			log.Errorf("Error parsing repository data: %v", err)
-			continue
-		}
+			d, err := e.parseRepo(ctx, *repo)
+			if err != nil {
+				return fmt.Errorf("parsing repository data: %v", err)
+			}
 
-		data = append(data, d)
+			data = append(data, d)
+			return nil
+		})
+	}
+
+	err := gRepos.Wait()
+	if err != nil {
+		return nil, fmt.Errorf("processing repositories: %v", err)
 	}
 
 	return data, nil
@@ -137,86 +149,124 @@ func (e *Exporter) getRepoMetrics(ctx context.Context) ([]*Datum, error) {
 // getUserMetrics fetches metrics for the configured users
 func (e *Exporter) getUserMetrics(ctx context.Context) ([]*Datum, error) {
 	var data []*Datum
-	for _, m := range e.Users {
-		repos, _, err := e.Client.Repositories.ListByUser(ctx, m, nil)
-		if err != nil {
-			log.Errorf("Error fetching user data: %v", err)
-			continue
-		}
 
-		for _, repo := range repos {
-			d, err := e.parseRepo(ctx, *repo)
+	gUsers, uCtx := errgroup.WithContext(ctx)
+	gUsers.SetLimit(e.FetchUsersConcurrency)
+
+	for _, m := range e.Users {
+		gUsers.Go(func() error {
+			log.Debugf("Fetching data for user: %s", m)
+			repos, _, err := e.Client.Repositories.ListByUser(uCtx, m, nil)
 			if err != nil {
-				log.Errorf("Error parsing user repository data: %v", err)
-				continue
+				return fmt.Errorf("fetching user data: %v", err)
 			}
 
-			data = append(data, d)
-		}
+			for _, repo := range repos {
+				d, err := e.parseRepo(ctx, *repo)
+				if err != nil {
+					return fmt.Errorf("parsing user repository data: %v", err)
+				}
 
+				data = append(data, d)
+			}
+			return nil
+		})
 	}
+
+	err := gUsers.Wait()
+	if err != nil {
+		return nil, fmt.Errorf("processing users: %v", err)
+	}
+
 	return data, nil
 }
 
 // getOrgMetrics fetches metrics for the configured organisations
 func (e *Exporter) getOrgMetrics(ctx context.Context) ([]*Datum, error) {
 	var data []*Datum
+
+	gOrgs, oCtx := errgroup.WithContext(ctx)
+	gOrgs.SetLimit(e.FetchOrgsConcurrency)
+
 	for _, m := range e.Organisations {
-		repos, _, err := e.Client.Repositories.ListByOrg(ctx, m, nil)
-		if err != nil {
-			log.Errorf("Error fetching organisation data: %v", err)
-			continue
-		}
+		gOrgs.Go(func() error {
+			log.Debugf("Fetching data for organisation: %s", m)
+			repos, _, err := e.Client.Repositories.ListByOrg(oCtx, m, nil)
+			if err != nil {
+				return fmt.Errorf("fetching organisation data: %v", err)
+			}
 
-		for _, repo := range repos {
-			d, err := e.parseRepo(ctx, *repo)
+			gRepos, rCtx := errgroup.WithContext(ctx)
+			gRepos.SetLimit(e.FetchOrgReposConcurrency)
+
+			for _, repo := range repos {
+				gRepos.Go(func() error {
+					d, err := e.parseRepo(rCtx, *repo)
+					if err != nil {
+						return fmt.Errorf("parsing organisation repository data: %w", err)
+					}
+
+					data = append(data, d)
+					return nil
+				})
+			}
+
+			err = gRepos.Wait()
 			if err != nil {
-				log.Errorf("Error parsing organisation repository data: %v", err)
-				continue
+				return fmt.Errorf("processing organisation repositories: %v", err)
 			}
+			return nil
+		})
+	}
 
-			data = append(data, d)
-		}
+	err := gOrgs.Wait()
+	if err != nil {
+		return nil, fmt.Errorf("processing organisations: %v", err)
 	}
 
 	return data, nil
 }
 
 func (e *Exporter) parseRepo(ctx context.Context, repo github.Repository) (*Datum, error) {
+	var releases []Release
+	var pulls []Pull
+
 	repoOwner := repo.GetOwner().GetLogin()
 	repoName := repo.GetName()
 
-	rel, _, err := e.Client.Repositories.ListReleases(ctx, repoOwner, repoName, nil)
-	if err != nil {
-		return nil, fmt.Errorf("listing releases: %w", err)
-	}
+	if e.FetchRepoReleasesEnabled {
+		log.Debugf("Fetching releases for repository: %s/%s", repoOwner, repoName)
+		rel, _, err := e.Client.Repositories.ListReleases(ctx, repoOwner, repoName, nil)
+		if err != nil {
+			return nil, fmt.Errorf("listing releases: %w", err)
+		}
 
-	var releases []Release
-	for _, release := range rel {
-		var assets []Asset
-		for _, asset := range release.Assets {
-			a := Asset{
-				Name:      asset.GetName(),
-				Size:      asset.GetSize(),
-				Downloads: asset.GetDownloadCount(),
-				CreatedAt: asset.GetCreatedAt().Format(time.RFC3339),
+		for _, release := range rel {
+			var assets []Asset
+			for _, asset := range release.Assets {
+				a := Asset{
+					Name:      asset.GetName(),
+					Size:      asset.GetSize(),
+					Downloads: asset.GetDownloadCount(),
+					CreatedAt: asset.GetCreatedAt().Format(time.RFC3339),
+				}
+				assets = append(assets, a)
 			}
-			assets = append(assets, a)
-		}
 
-		r := Release{
-			Name:   release.GetName(),
-			Tag:    release.GetTagName(),
-			Assets: assets,
+			r := Release{
+				Name:   release.GetName(),
+				Tag:    release.GetTagName(),
+				Assets: assets,
+			}
+			releases = append(releases, r)
 		}
-		releases = append(releases, r)
 	}
 
+	log.Debugf("Fetching pull requests for repository: %s/%s", repoOwner, repoName)
 	pullRequests, _, err := e.Client.PullRequests.List(ctx, repoOwner, repoName, nil)
 	if err != nil {
 		return nil, fmt.Errorf("fetching pull requests: %w", err)
 	}
-	var pulls []Pull
 	for _, pr := range pullRequests {
 		p := Pull{
 			Url: pr.GetURL(),
diff --git a/go.mod b/go.mod
index ea67358..9adf8f4 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/sirupsen/logrus v1.9.3
 	github.com/steinfletcher/apitest v1.3.8
 	github.com/stretchr/testify v1.11.1
+	golang.org/x/sync v0.15.0
 )
 
 require (
diff --git a/go.sum b/go.sum
index 3940cae..c401789 100644
--- a/go.sum
+++ b/go.sum
@@ -58,6 +58,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
 go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
 golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=



Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 3072798..d77f346 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -25,31 +25,41 @@ func TestConfig(t *testing.T) {
 					Scheme: "https",
 					Host:   "api.github.com",
 				},
-				Repositories:           []string{},
-				Organisations:          []string{},
-				Users:                  []string{},
-				GitHubResultsPerPage:   100,
-				GithubToken:            "",
-				GithubTokenFile:        "",
-				GitHubApp:              false,
-				GitHubAppConfig:        nil,
-				GitHubRateLimitEnabled: true,
+				Repositories:             []string{},
+				Organisations:            []string{},
+				Users:                    []string{},
+				GitHubResultsPerPage:     100,
+				GithubToken:              "",
+				GithubTokenFile:          "",
+				GitHubApp:                false,
+				GitHubAppConfig:          nil,
+				GitHubRateLimitEnabled:   true,
+				FetchRepoReleasesEnabled: true,
+				FetchOrgsConcurrency:     1,
+				FetchOrgReposConcurrency: 1,
+				FetchReposConcurrency:    1,
+				FetchUsersConcurrency:    1,
 			},
 			expectedErr: nil,
 		},
 		{
 			name: "non-default config",
 			envVars: map[string]string{
-				"METRICS_PATH":              "/otherendpoint",
-				"LISTEN_PORT":               "1111",
-				"LOG_LEVEL":                 "DEBUG",
-				"API_URL":                   "https://example.com",
-				"REPOS":                     "repo1, repo2",
-				"ORGS":                      "org1,org2 ",
-				"USERS":                     " user1, user2 ",
-				"GITHUB_RESULTS_PER_PAGE":   "50",
-				"GITHUB_TOKEN":              "token",
-				"GITHUB_RATE_LIMIT_ENABLED": "false",
+				"METRICS_PATH":                "/otherendpoint",
+				"LISTEN_PORT":                 "1111",
+				"LOG_LEVEL":                   "DEBUG",
+				"API_URL":                     "https://example.com",
+				"REPOS":                       "repo1, repo2",
+				"ORGS":                        "org1,org2 ",
+				"USERS":                       " user1, user2 ",
+				"GITHUB_RESULTS_PER_PAGE":     "50",
+				"GITHUB_TOKEN":                "token",
+				"GITHUB_RATE_LIMIT_ENABLED":   "false",
+				"FETCH_REPO_RELEASES_ENABLED": "false",
+				"FETCH_ORGS_CONCURRENCY":      "2",
+				"FETCH_ORG_REPOS_CONCURRENCY": "3",
+				"FETCH_REPOS_CONCURRENCY":     "4",
+				"FETCH_USERS_CONCURRENCY":     "5",
 			},
 			expectedCfg: &Config{
 				MetricsPath: "/otherendpoint",
@@ -71,12 +81,17 @@ func TestConfig(t *testing.T) {
 					"user1",
 					"user2",
 				},
-				GitHubResultsPerPage:   50,
-				GithubToken:            "token",
-				GithubTokenFile:        "",
-				GitHubApp:              false,
-				GitHubAppConfig:        nil,
-				GitHubRateLimitEnabled: false,
+				GitHubResultsPerPage:     50,
+				GithubToken:              "token",
+				GithubTokenFile:          "",
+				GitHubApp:                false,
+				GitHubAppConfig:          nil,
+				GitHubRateLimitEnabled:   false,
+				FetchRepoReleasesEnabled: false,
+				FetchOrgsConcurrency:     2,
+				FetchOrgReposConcurrency: 3,
+				FetchReposConcurrency:    4,
+				FetchUsersConcurrency:    5,
 			},
 			expectedErr: nil,
 		},
diff --git a/test/github_exporter_test.go b/test/github_exporter_test.go
index ad55001..98a507f 100644
--- a/test/github_exporter_test.go
+++ b/test/github_exporter_test.go
@@ -80,7 +80,6 @@ func TestGithubExporterHttpErrorHandling(t *testing.T) {
 		githubRepos(),
 		githubReleases(),
 		githubPullsError(),
-		githubRateLimit(),
 	).
 		Get("/metrics").
 		Expect(t).