# Rate Limiting Scans That Run Through a Private Location

Private Locations often target internal infrastructure that's sensitive to bursts of scanner traffic: corporate monitoring agents (Datadog or New Relic), SIEMs, WAFs, or test databases. Set a rate limit that your targets and monitoring systems can handle.

Use the recipes below to apply a consistent profile rate limit across a Private Location's scans. Browser crawling uses separate controls, described in [Scope and Traffic Controls](<#scope-and-traffic-controls>).

> [!TIP]
>
> **Set the Rate on Each Profile**
>
> Control the scan rate with `network.requests_per_second` on each profile using the Private Location. Apply it via `PUT /v3/profiles/{profileId}/configuration`, and reapply it whenever new profiles are created. See [Recipe 1](<#recipe-1-one-shot-rate-limit-across-profiles>) below.

## Profile-Based Rate Limiting

Set Escape's scan rate in the **profile configuration** with `network.requests_per_second`. A Private Location is a transport: it gives the scanner access to your network. The profile rate limit applies to API DAST requests, ASM HTTP requests, and port scanner probes, whether the profile uses an Escape-managed location or a Private Location. Configure browser crawling with its separate controls in [Scope and Traffic Controls](<#scope-and-traffic-controls>).

This keeps behavior explicit and reproducible:

- Profiles can use the same rate limit across different locations.
- A scan's configured rate is visible in the profile configuration and stays the same across locations.
- Per-profile rate limits can diverge (slow scans for internal monitoring-sensitive assets, fast scans for public staging).

To apply the same rate to all profiles using a Private Location, use the recipes below to update those profiles together.

## Prerequisites

- An API key with profile write access. Generate one from your [user profile](<https://app.escape.tech/user/profile/>).
- The ID of the Private Location you want to throttle, either from `escape-cli locations list` or from the [Private Locations page](<https://app.escape.tech/organization/private-locations/>) in the UI.
- A way to identify the profiles bound to that location. Select them by tag, name, or domain on `GET /v3/profiles`. Its response doesn't include `proxyId`, so choose a convention before starting:

  - **Tag-based (recommended)**: tag every profile you create against the Private Location with a stable tag such as `location:site-internal`. This makes the set explicit, searchable in the UI, and filterable via the API.
  - **Name-based**: prefix the profile name (for example `[Internal]` or `[MySite]`). Filter with the `search` query parameter on `GET /v3/profiles`.
  - **Domain-based**: filter with the `domains` query parameter when all internal assets live under a distinct domain (for example `*.internal.site.com`).
  - **Exhaustive**: apply the rate limit to every profile in the organization. Appropriate when the Private Location is the dominant scan path (for example, the whole organization scans only internal assets).

## Recipe 1: One-Shot Rate Limit Across Profiles

Applies `network.requests_per_second` to every profile matching the selection convention you chose. Run it once after the initial rollout.

The script below assumes **tag-based** selection: replace `TAG_ID` and `RATE` to fit your environment. Both filtering variants (by `search` or `domains`) work with the same pattern; only the query string changes.

```bash
#!/usr/bin/env bash
set -euo pipefail

API_KEY="${ESCAPE_API_KEY:?export ESCAPE_API_KEY first}"
API="https://public.escape.tech/v3"

TAG_ID="00000000-0000-0000-0000-000000000000"   # tag marking "internal" profiles
RATE=10                                         # requests_per_second to enforce

# 1. List every profile carrying the tag, walking the cursor.
cursor=""
while :; do
  page=$(curl -fsS -G "$API/profiles" \
    -H "X-ESCAPE-API-KEY: $API_KEY" \
    --data-urlencode "tagIds=$TAG_ID" \
    --data-urlencode "size=100" \
    --data-urlencode "cursor=$cursor")

  echo "$page" | jq -r '.data[].id' | while read -r profile_id; do
    # 2. Read the current configuration, override requests_per_second, write it back.
    current=$(curl -fsS "$API/profiles/$profile_id" \
      -H "X-ESCAPE-API-KEY: $API_KEY" | jq '.configuration')

    updated=$(jq --argjson rate "$RATE" \
      '.network = (.network // {}) | .network.requests_per_second = $rate' \
      <<< "$current")

    curl -fsS -X PUT "$API/profiles/$profile_id/configuration" \
      -H "X-ESCAPE-API-KEY: $API_KEY" \
      -H "Content-Type: application/json" \
      -d "$(jq -n --argjson cfg "$updated" '{configuration: $cfg}')" \
      > /dev/null

    echo "Updated $profile_id -> requests_per_second=$RATE"
  done

  cursor=$(echo "$page" | jq -r '.nextCursor // empty')
  [[ -z "$cursor" ]] && break
done
```

What the script does:

1. Pages through `GET /v3/profiles` filtered by a tag that identifies profiles bound to the Private Location.
2. For each profile, fetches the full configuration with `GET /v3/profiles/{profileId}`.
3. Merges `network.requests_per_second` into the existing configuration (preserving other settings, such as custom headers, authentication, and scope).
4. Writes the new configuration back with `PUT /v3/profiles/{profileId}/configuration`.

> [!WARNING]
>
> **Merge, Don't Overwrite**
>
> `PUT /v3/profiles/{profileId}/configuration` replaces the **entire** configuration body with what you send. Always read the current configuration first, patch only the fields you want to change, and write the merged object back. The `jq` step in the script above is the minimal safe pattern.

> [!NOTE]
>
> **Valid Range for `requests_per_second`**
>
> The scanner configuration schema defaults to `100` and allows integers from `1` to `1000`. New DAST profiles created in the UI start at `500`; the UI rate slider allows `10` to `500`. Set the rate explicitly for internal targets, then check the effect on your target and monitoring systems before raising it.

## Recipe 2: Keep It Enforced for Newly Created Profiles

Recipe 1 updates existing profiles. New profiles need an explicit rate limit too: defaults differ between the scanner schema and the UI. Two approaches keep the rate limit in place as the environment evolves.

### Option A: Enforce at Creation Time

Set the field inline when the profile is created, either via the DAST creation UI (**Fine-Tune (Optional)** → **Duration** → **Rate limit**) or via the API:

```bash
API_KEY="${ESCAPE_API_KEY:?export ESCAPE_API_KEY first}"
API="https://public.escape.tech/v3"

curl -X POST "$API/profiles/rest" \
  -H "X-ESCAPE-API-KEY: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "assetId": "<ASSET_ID>",
    "extraAssetIds": ["<SCHEMA_ID>"],
    "name": "[Internal] Customer Service API",
    "proxyId": "<PRIVATE_LOCATION_ID>",
    "tagsIds": ["<INTERNAL_TAG_ID>"],
    "mode": "read_only",
    "configurationObject": {
      "network": {
        "requests_per_second": 10
      }
    }
  }'
```

Any team that creates profiles against the Private Location should use this shape. If profile creation is scripted or templated, update the template once.

### Option B: Periodic Re-Enforcement Job

To apply the rate to new profiles that follow your tag or name convention, wrap Recipe 1 in a scheduled job (GitHub Actions, GitLab CI schedules, or a Kubernetes `CronJob`) that runs nightly or hourly. It's idempotent: profiles already at the target rate are PUT with the same value and keep their configuration.

```yaml
# .github/workflows/enforce-private-location-rate-limit.yml
name: Enforce private-location rate limit
on:
  schedule:
    - cron: "0 2 * * *"   # every night at 02:00 UTC
  workflow_dispatch:
jobs:
  enforce:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Apply rate limit
        env:
          ESCAPE_API_KEY: ${{ secrets.ESCAPE_API_KEY }}
        run: ./scripts/enforce-rate-limit.sh   # the script from Recipe 1
```

## Scope and Traffic Controls

- **Combined traffic across scans.** Control the combined rate through each profile's `network.requests_per_second` and the number of concurrent scans. Two concurrent scans each configured for 100 requests/second have a combined configured rate of 200 requests/second; actual throughput depends on connections and the target. These are per-profile settings, with no separate per-location requests-per-second ceiling. Concurrent connection limits close excess connections independently of the rate settings. See [Resource Management](<https://docs.escape.tech/documentation/platform/private-location/resource-management/#is-it-better-to-deploy-multiple-private-locations-or-increase-resources-on-a-single-instance>).
- **Sensitive endpoints and domains.** `network.requests_per_second` applies across API DAST requests, ASM HTTP requests, and port scanner probes. The rate is configured per profile, rather than separately per endpoint or domain. Use the [API Testing blocklist](<https://docs.escape.tech/documentation/dast/webapp-testing/scope/#api-testing-scope-configuration>) to exclude specific sensitive paths from API testing.
- **Browser crawling.** Control crawling traffic (the browser fetching pages) with `frontend_dast.parallel_workers` and related settings. Crawling uses these controls separately from `network.requests_per_second`. See [WebApp Testing: Performance Tuning](<https://docs.escape.tech/documentation/dast/webapp-testing/performance-tuning/index.md>) for the full model.

## Related Documentation

- [Public API](<https://docs.escape.tech/documentation/automate/public-api/index.md>): authentication, base URL, and other recipes
- [Profiles Management](<https://docs.escape.tech/documentation/automate/cli/profiles/index.md>): creating and listing profiles via the CLI
- [API DAST: Rate Limiting](<https://docs.escape.tech/documentation/dast/api-testing/rate-limiting/index.md>): scanner-side rate limiting reference
- [WebApp DAST: Performance Tuning](<https://docs.escape.tech/documentation/dast/webapp-testing/performance-tuning/index.md>): WebApp-specific throttling
- [Private Location Logging &amp; Monitoring](<https://docs.escape.tech/documentation/platform/private-location/logging-monitoring/index.md>): observing Private Location behavior in production
