Team managed throttling of feature flag requests by distinct_id
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Problem
A feature flags customer recently needed a small number of their users throttled because those users were generating disproportionate request volume. The customer identified them by distinct_id. We had no way to express that directly, so we resolved each distinct_id to a source IP and hardcoded the IP into Envoy's global rate limit configuration. This was done in charts#9712 (shadow) and charts#9720 (enforce).
This approach has several problems. We end up limiting IPs when the customer asked us to limit users, which is the wrong identifier because IPs rotate through NATs, mobile carriers, and shared CGNAT, so the rule hits bystanders and misses the real offenders. Envoy sees only an IP and a path, so the rule cannot be scoped to a single team. Every new entry requires a PostHog engineer to edit YAML in charts/ and wait on ArgoCD, which does not scale to many users or many customers. The rule tree on the shared ratelimit service also grows with each unrelated customer request.
Proposed solution
Give each team a first class list of distinct_ids to block or throttle at the feature flags service itself. Customers manage the list through the normal team settings API. The list is shipped to the Rust feature flags service via the same HyperCache mechanism already used to distribute flag definitions, so the hot path only does an in memory lookup.
High level design
- Backend model. Add a
TeamFeatureFlagRateLimitConfigTeam extension (per the pattern inposthog/models/team/README.md) with three fields: a blocklist of distinct ids, an optional per id rate spec for soft throttling, and an enforcement mode (off,shadow,enforce). Expose the config on the existing Team API with admin level field access control. - Distribution. A Celery task pushes the config to a new HyperCache namespace keyed by team api token on every change, mirroring how
flag_definitions_hypercachealready works. Redis plus S3 backed, roughly 30 second propagation. - Enforcement. The Rust feature flags service loads each team's config from HyperCache into a
HashSet(blocklist) and aHashMapofgovernor::Quota(rate limits). The check runs inhandler::process_requestimmediately aftercookieless::handle_distinct_idresolves the final identifier and before flag fetch and evaluation. Blocked requests return an empty flags response with arate_limited=truemarker so SDKs degrade gracefully. Throttled requests reuse the existingFlagsRateLimiterplumbing. - Shadow mode.
shadowevaluates the rule and emits metrics and a canonical log field but still returns real flag results. Each team can validate its list before flipping toenforce, without a global environment variable toggle. - Observability. New metric
flags_distinct_id_limit_evaluated_total{team_id, mode, action}for allow, shadow block, enforced block, shadow throttle, enforced throttle. Add a panel tofeature-flags-general.json. The canonical log gains adistinct_id_rate_limitedfield so Loki queries can answer whether a specific request was blocked by a team rule. - Failure mode. If HyperCache is unavailable, fail open and allow the request, matching the existing billing limiter posture.
Performance
The hot path per request is one DashMap read on the HyperCache reader, one HashSet::contains, and optionally one governor::check_key if the id is also soft throttled. No Redis or Postgres call per request. Config refresh happens on a background interval.
Migration
Ship with mode=off as the default so no team is affected at deploy time. For the currently hardcoded customer, populate their config with the distinct ids that motivated the original IP rule, run in shadow for roughly 24 hours and compare against the Envoy counter in Grafana, then flip to enforce and revert the IP rule in charts.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with posthog/models/team/README.md and trace the existing flag_definitions_hypercache distribution flow. Then inspect handler::process_request, cookieless::handle_distinct_id, FlagsRateLimiter, and feature-flags-general.json to understand enforcement and observability entry points. Done means team-managed distinct_id blocklists and throttles support off, shadow, and enforce modes with fail-open behavior and the specified metrics and logs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- grafana, python, redis, rust
- Domain
- api, backend, distributed-systems, observability
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100