Skip to content

feat: introduce SNYK_REQUEST_CONCURRENCY for dependency request parallelism#6756

Merged
bgardiner merged 6 commits into
mainfrom
feat/snyk-request-concurrency
May 14, 2026
Merged

feat: introduce SNYK_REQUEST_CONCURRENCY for dependency request parallelism#6756
bgardiner merged 6 commits into
mainfrom
feat/snyk-request-concurrency

Conversation

@bgardiner
Copy link
Copy Markdown
Contributor

@bgardiner bgardiner commented Apr 29, 2026

What does this PR do?

Adds a tunable concurrency knob for in-flight dependency-test / dependency-monitor HTTP requests, configurable via the user-facing SNYK_REQUEST_CONCURRENCY env var (default 5, clamped to [1, 50]).

The default is unchanged from the prior hard-coded MAX_CONCURRENCY = 5 — this PR is purely a configurability change. Bumping the default can be revisited separately once we have telemetry on CPU and rate-limit impact (per the review thread).

The user-facing env var is owned by the Go CLI's GAF configuration, with the resolved value forwarded to the legacy CLI via the internal SNYK_INTERNAL_REQUEST_CONCURRENCY env var. This keeps Go as the single source of truth for application configuration (env vars, future config files / flags, etc.), per Peter's review feedback.

Wiring

  • Go side
    • cliv2.ConfigKeyRequestConcurrency (internal_request_concurrency) — new GAF config key.
    • main.go registers snyk_request_concurrency as an alternative key, so the env var feeds the config.
    • fillEnvironmentFromConfig writes the resolved value to SNYK_INTERNAL_REQUEST_CONCURRENCY for the legacy CLI process.
    • The internal var is added to the legacy-CLI env blacklist so a user can't bypass the Go config by setting it directly.
  • TS side
    • getRequestConcurrency() in src/lib/snyk-test/common.ts reads SNYK_INTERNAL_REQUEST_CONCURRENCY (the internal contract from Go), defaults to 5, clamps to [1, 50].
    • Replaces the hard-coded MAX_CONCURRENCY = 5 constant at the existing pMap call site in sendAndParseResults (src/lib/snyk-test/run-test.ts).

A follow-up PR (#6757) adopts the same helper in src/lib/ecosystems/monitor.ts:monitorDependencies, which is currently fully sequential.

Why?

snyk container test produces one ScanResult per directory of dependencies in the image (lib/analyzer/applications/java.ts:groupJarFingerprintsByPath in snyk-docker-plugin). For Java-heavy images this can be hundreds of ScanResults, each becoming a separate POST /test-dependencies request. With the prior hard-coded concurrency cap of 5, the request fan-out is the dominant wall-clock cost — and there was no escape hatch.

This PR adds the escape hatch without changing default behavior:

  • Single-project tests and small --all-projects runs are unaffected.
  • Container tests and large --all-projects runs default to the same throughput as today, but can opt into higher concurrency via the env var.

Benchmark — quay.io/wildfly/wildfly:34.0.1.Final-jdk21 (512 ScanResults)

hyperfine --warmup 1 --runs 3 against a locally-built PR binary, varying only SNYK_REQUEST_CONCURRENCY. The c=5 row is the default and reproduces the prior hard-coded behavior.

Configuration Mean wall-clock vs default
default (c=5) 68.04 s ± 1.72 s 1.00×
override (c=10) 40.96 s ± 0.15 s 1.66× faster (−40%)
override (c=20) 25.13 s ± 0.48 s 2.71× faster (−63%)

Min/max within ±2% of mean across all configs — variance is tight. Standard deviation drops with higher concurrency (1.72s → 0.15s → 0.48s) because the wait-time component shrinks and the scan becomes more CPU-bound on the (much steadier) JAR-extraction work in snyk-docker-plugin.

(I re-validated end-to-end on the latest revision with 2 hyperfine runs per config; min(c=20)=53s < min(c=5)=64s confirms the env var continues to flow through Go → TS as expected.)

Where should the reviewer start?

  • cliv2/internal/cliv2/cliv2.go — new exported ConfigKeyRequestConcurrency; fillEnvironmentFromConfig forwarding; blacklist entry.
  • cliv2/internal/constants/constants.goSNYK_INTERNAL_REQUEST_CONCURRENCY_ENV constant.
  • cliv2/cmd/cliv2/main.goAddAlternativeKeys registration for the user-facing env var name.
  • src/lib/snyk-test/common.tsgetRequestConcurrency() helper.
  • src/lib/snyk-test/run-test.ts — single call-site swap.
  • cliv2/internal/cliv2/cliv2_test.goTest_PrepareV1EnvironmentVariables_RequestConcurrency (3 sub-cases: forwarded-when-set, omitted-when-unset, user-set-internal-var-stripped-and-reapplied).
  • test/jest/unit/lib/snyk-test/common.spec.ts — 9 unit tests for the helper covering default, override, clamping, and invalid-input cases.

How should this be manually tested?

  1. Run targeted unit tests:
    npx jest --selectProjects coreCli --testPathPattern 'test/jest/unit/lib/snyk-test/common.spec'
    cd cliv2 && go test ./internal/cliv2/ -run Test_PrepareV1EnvironmentVariables_RequestConcurrency
  2. Optionally measure on a representative many-ScanResult image (Wildfly works well):
    hyperfine --warmup 1 --runs 3 \
      './binary-releases/snyk-... container test quay.io/wildfly/wildfly:34.0.1.Final-jdk21' \
      'SNYK_REQUEST_CONCURRENCY=10 ./binary-releases/snyk-... container test quay.io/wildfly/wildfly:34.0.1.Final-jdk21' \
      'SNYK_REQUEST_CONCURRENCY=20 ./binary-releases/snyk-... container test quay.io/wildfly/wildfly:34.0.1.Final-jdk21'
    (Use the Go-wrapped binary, not node bin/snyk directly — the Go wrapper is what reads SNYK_REQUEST_CONCURRENCY and forwards it as SNYK_INTERNAL_REQUEST_CONCURRENCY.)

Risk assessment

Low. No default behavior change. The override is bounded to [1, 50]. The Go-side wiring is additive: the legacy CLI's env handling stays the same except for the new internal var (covered by the new Go test).

Background

Supersedes #6747 (which targeted the wrong code path — src/lib/ecosystems/test.ts:testDependencies is unreachable from snyk container test; getEcosystemForTest returns null for docker, so container test goes through runTest/pMap instead).

…lelism

Add a tunable concurrency knob for in-flight dependency-test/dependency-monitor
HTTP requests, default 10 (raised from the prior hard-coded 5), clamped to
[1, 50]. Override via the SNYK_REQUEST_CONCURRENCY environment variable.

Apply the new helper at the existing pMap call site in
sendAndParseResults (run-test.ts). The default bump is effectively a no-op
for non-container test workloads (single-project tests produce one payload;
--all-projects rarely produces more than the prior 5-payload ceiling), but
materially improves wall-clock for container tests that produce one
ScanResult per directory containing dependencies.

A follow-up PR will adopt the same helper in the container monitor path,
which is currently fully sequential.
@snyk-io
Copy link
Copy Markdown

snyk-io Bot commented Apr 29, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

Warnings
⚠️ There are multiple commits on your branch, please squash them locally before merging!
⚠️

"feat: introduce SNYK_REQUEST_CONCURRENCY for dependency request parallelism" is too long. Keep the first line of your commit message under 72 characters.

⚠️

"feat: introduce SNYK_REQUEST_CONCURRENCY for dependency request parallelism" is too long. Keep the first line of your commit message under 72 characters.

Generated by 🚫 dangerJS against abe1f81

@bgardiner
Copy link
Copy Markdown
Contributor Author

Benchmark — quay.io/wildfly/wildfly:34.0.1.Final-jdk21 (512 ScanResults)

hyperfine --warmup 1 --runs 3 against the same locally-built PR A binary, varying only SNYK_REQUEST_CONCURRENCY to isolate the API concurrency change from any other build/runtime differences. The c=5 row reproduces the prior hard-coded value, so it's an apples-to-apples baseline.

Configuration Mean wall-clock vs baseline
baseline (c=5) 68.04 s ± 1.72 s 1.00×
PR A default (c=10) 40.96 s ± 0.15 s 1.66× faster (−40%)
PR A override (c=20) 25.13 s ± 0.48 s 2.71× faster (−63%)

Each config: 3 runs after 1 warmup; image pre-pulled. Min/max within ±2% of mean across all configs — variance is tight.

Standard deviation drops with higher concurrency (1.72s → 0.15s → 0.48s) because the wait-time component shrinks and the scan becomes more CPU-bound on the (much steadier) JAR-extraction work in snyk-docker-plugin.

@bgardiner bgardiner marked this pull request as ready for review April 30, 2026 01:21
@bgardiner bgardiner requested review from a team as code owners April 30, 2026 01:21
@bgardiner bgardiner requested a review from bdemeo12 April 30, 2026 01:22
@snyk-pr-review-bot

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@bdemeo12 bdemeo12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same questions as this PR: #6757

But LGTM

Comment thread src/lib/snyk-test/common.ts
Comment thread src/lib/snyk-test/common.ts Outdated
export const RETRY_ATTEMPTS = 3;
export const RETRY_DELAY = 500;

const DEFAULT_REQUEST_CONCURRENCY = 10;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking: Generally changing a default without visibility in the behaviour, we currently don't have any metrics on CPU consumption, makes me wonder if we should be more careful. The impact is quite huge as it affects all SCA and Container scans.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there'll be significant CPU impact. Node executes JS on a single thread, and the code path this PR touches spends nearly all its time waiting on HTTP responses, not in JS execution.

On the "affects all SCA and Container scans" concern, I agree. A few things that make me comfortable with the bump:

  • headroom to throttling is seemingly large. I put more discussion here. The api-gateway rate limit on /v1/test* and /v1/monitor* is keyed per principal_id (per-token), default high bucket: 200 req/s burst, 2000/min, 60000/hour. Even at the override ceiling (50), a single CLI scan stays at ≤25% of the per-second cap.
  • 5 is a conservatively low bound and 10 is still quite modest as a default.
  • OS flows team reviewed and approved the PR 😄

Other options:

  • we could land a 5 to 8 bump first and revisit after we have telemetry?
  • add a feature-flag-style override/escape hatch to bump it back to 5
  • any other ideas?

What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense for a single CLI run. A lot of our user actually run the CLI highly concurrent and already experience rate limiting. In the combination in the best case, the increase of request parallelism and rate limiting might just equal out, in the worst case the rate limiting will eventually exceed the retry limit and fail the CLI.

Address review feedback on the test/monitor request-concurrency knob:

- Restore the default to 5 (the prior hard-coded value), so the env-var
  introduction is purely a configurability change. The default-bump
  question can be revisited separately once we have telemetry, per
  Peter's review.
- Make the GAF configuration the single source of truth for the
  user-facing SNYK_REQUEST_CONCURRENCY value: register a new
  cliv2.ConfigKeyRequestConcurrency key, with snyk_request_concurrency
  as an alternative key (so the env var feeds the config). The Go side
  forwards the resolved value to the legacy CLI process via the
  internal SNYK_INTERNAL_REQUEST_CONCURRENCY env var. The TS helper now
  reads that internal env var instead of the user-facing one, leaving
  the public configuration surface owned by Go (and reachable in the
  future from config files / flags without further TS changes).
- Add the new internal env var to the legacy-CLI env blacklist so a
  user can't bypass the Go config by setting it directly.
- A new env var (not the existing MAX_THREADS) keeps HTTP request
  concurrency separate from the CPU-bound thread pool, per F2F.
@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

Add unit coverage for fillEnvironmentFromConfig's handling of the new
ConfigKeyRequestConcurrency: forwards a user-set value to the legacy
CLI as SNYK_INTERNAL_REQUEST_CONCURRENCY, omits the internal env when
unset, and strips a user-provided internal env so Go remains the
source of truth.
@snyk-pr-review-bot

This comment has been minimized.

@bgardiner bgardiner enabled auto-merge May 13, 2026 18:02
@snyk-pr-review-bot

This comment has been minimized.

@bgardiner bgardiner requested a review from PeterSchafer May 13, 2026 21:57
Under main.go's WithSupportedEnvVarPrefixes setup (the production
config), GAF's IsSet does not pre-bind env vars for alternative keys —
only get() does. As a result, config.IsSet(ConfigKeyRequestConcurrency)
returned false even when SNYK_REQUEST_CONCURRENCY was set, so the
internal env var was never forwarded to the legacy CLI process and the
TS code always saw the default concurrency.

Switch to GetString and check non-empty: GetString goes through GAF's
get(), which binds the alt key before reading.

The original unit test passed only because it used WithAutomaticEnv,
which bypasses bindEnv entirely and so masked the production behavior.
Update the test to construct the config the way main.go does (with
WithSupportedEnvVarPrefixes), so the regression is caught next time.
@snyk-pr-review-bot
Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected
📚 Repository Context Analyzed

This review considered 13 relevant code sections from 6 files (average relevance: 0.82)

@bgardiner bgardiner merged commit c683470 into main May 14, 2026
9 checks passed
@bgardiner bgardiner deleted the feat/snyk-request-concurrency branch May 14, 2026 08:12
Comment thread cliv2/internal/cliv2/cliv2.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants