To attribute feature-flag API cost to cohorts in a Node.js service, record flag evaluations and configuration refreshes as separate events. Tag each one with a stable pseudonymous cohort ID, the configuration version or ETag in effect, and an outcome that includes failures, retries and rate limits. Then apply one written rule for splitting shared polling work, and keep metric labels bounded so the cohort breakdown survives the telemetry pipeline.
Start with the billing unit, not the API call
“Feature-flag API request” is not a universal billing unit, so the first design decision is which of your calls a provider actually charges for. PostHog’s cutting-costs documentation states the server-side rule this way:
For server-side SDKs (Node, Python, PHP, etc.), each call that evaluates flags, such as
evaluateFlags(),evaluate_flags(),getFeatureFlag(),getAllFlags(), orisFeatureEnabled(), makes a request to the/flagsendpoint and incurs a billable event unless local evaluation resolves it.
Local evaluation moves the cost rather than removing it. The SDK checks flags against definitions it has already downloaded, so the per-check request goes away, but definition polling becomes its own charged activity. The same PostHog page states that $feature_flag_called events are not its billing basis, so exposure analytics cannot stand in for billable evaluations.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstall#1 Best Overall
| Event family | What it records | Billing relevance |
|---|---|---|
| Evaluation | A flag check made by application code | PostHog: each server-side call is billable unless local evaluation resolves it |
| Definition refresh | A poll for flag definitions or configuration | PostHog: local-evaluation polling is charged. Atlassian’s Forge SDK page describes a 60-second update poll but does not establish a billing rule |
| Retry attempt | A repeated refresh after a timeout, server error or rate limit | Provider-dependent; not stated in the PostHog or Atlassian pages cited here. Count it anyway, because it consumes capacity |
| Optional analytics event | Exposure telemetry such as $feature_flag_called |
Not PostHog’s billing basis |
Treat this table as a list of questions to put to your provider, not as a price list. Confirm the rules for your exact SDK version and plan before you map any event to an invoice line.
Define three event families
Use separate event families rather than one “flag cost” counter. A single counter cannot tell you whether a spike came from application traffic, a refresh loop or a retry storm.
flag_evaluation
Emit one event per evaluation initiated by application behavior. Include the provider, the SDK mode (remote or local), a bounded flag set or category, the result, the cohort, and the configuration version where the SDK exposes it. A local evaluation never leaves the process, but it still matters for behavior analysis. Whether it is billable depends on the provider rule above, so record the mode and let the billing view decide.
flag_config_refresh
Emit one event per poll attempt, covering every outcome: an unchanged response (including ETag-validated unchanged definitions where the SDK supports them), a changed configuration, an error, a timeout, or a rate limit. This is the family most often missing from dashboards, and it is where shared cost accumulates.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsRetry attempts
Record retries as an attempt_number attribute on the refresh event rather than as a separate family. The attempt chain for one refresh then stays together in a single query, and each attempt is recorded with the backoff that preceded it.
Rank #2
Fields to record
provider,sdk_mode,environmentcohort_id: a stable pseudonymous identifier, never a raw tenant or user IDconfig_version: the configuration version or ETag in effect when the event occurredattempt_number,outcome,http_status_class,retry_reason(timeout, rate limit, server error, network)backoff_msandretry_after_bucket, the latter as a range such as 0 to 1 s, 1 to 10 s, 10 to 60 s or above 60 s rather than the raw valuedurationandobserved_atallocation_basis: the name of the shared-cost rule applied to the row
Keep the full cohort_id in logs or traces with restricted access. Broadly exported metrics should carry only the bounded labels described later in this article.
Split shared polling work with one written rule
A definition poll that serves several cohorts is a shared cost, and any cohort comparison built on it depends on how that cost is split. Choose one rule before you compare cohorts:
- Equal allocation: each cohort served by a refresh receives an equal share.
- Volume allocation: each cohort’s share follows its evaluation count in the same window.
- Direct assignment: the poll serves exactly one cohort, so it is charged to that cohort in full.
Illustrative arithmetic, not a measurement: one poll costing 1 unit serves three cohorts with 600, 300 and 100 evaluations in the window. Equal allocation gives each cohort about 0.33 units. Volume allocation gives 0.6, 0.3 and 0.1. The totals agree because the rule only moves cost between cohorts, so the choice changes which cohort looks expensive.
Keep raw attempt totals in storage, apply the allocation when you build the cohort view, and write the rule into allocation_basis. If you later change the rule, you recompute the view from the raw totals instead of rewriting history.
Choose a polling topology
Where polling happens determines how many requests you make, how failures spread, and how cleanly cost can be attributed.
| Design axis | Central poller or shared cache | Per-process polling | Provider local-evaluation SDK |
|---|---|---|---|
| API request fan-out | Lowest when many processes share one source | Grows with process or instance count | Depends on the provider’s polling and cache behavior |
| Configuration freshness | Set by one poll cadence and its propagation | Each process refreshes on its own schedule | Set by the SDK’s refresh policy |
| Failure boundary | A shared component becomes critical infrastructure | Isolated per instance, but retries multiply across instances | The SDK handles some mechanics; monitoring is still required |
| Cohort cost attribution | Needs an explicit allocation of shared polls | Direct when each process serves one cohort; otherwise shared | Evaluation and refresh charges must be separated according to provider rules |
| Operational fit | Suits environments with a reliable cross-process cache | Simple to deploy; costly at scale | Suits environments where the vendor’s behavior and pricing fit the requirements |
No design wins independently of deployment topology, quota, freshness tolerance, pricing and failure behavior.
Provider controls to check
PostHog’s local-evaluation documentation describes three controls: ETag requests for unchanged definitions, a longer polling interval, and sharing definitions across instances. Its default definition polling interval is 30 seconds, and the same page notes the freshness cost of a longer interval. It also warns against local evaluation in edge or Lambda-style contexts where an instance may be initialized per invocation, since each invocation may then repeat the download. PostHog ties ETag support to Node SDK 5.17.2 as of its 2026 documentation, so check your installed version’s release notes before relying on it.
Atlassian’s Forge feature-flag SDK documentation, last updated May 18, 2026, describes locally cached evaluations with configuration update polls 60 seconds apart after initialization. That is the cadence of that platform’s SDK, not a general default. The Forge feature-flag SDK page is the authoritative reference for it.
Freshness versus request budget
Polling volume is roughly the number of instances multiplied by the number of polls each instance makes, so the interval is your main budget lever. It is also your main freshness cost: a configuration change cannot reach a process faster than that process’s next successful refresh.
PostHog’s documentation gives one worked example. At its 30-second default interval, a continuously running server makes 86,400 unchanged polling requests per month, plus 10 requests for each poll that returns new definitions. That is the vendor’s own arithmetic, not an independent measurement. Doubling the interval to 60 seconds would halve the unchanged-poll count to 43,200 per server per month under the same 30-day assumption. That 43,200 figure is derived here from PostHog’s example; PostHog does not state it.
Rank #4
Turn the trade-off into a rule before you pick an interval:
Free tools Windows power users keep installed
One-click scans. No signup required.
- Set a maximum snapshot age for each flag category, based on how quickly that category must propagate. A safety kill switch and a cosmetic experiment rarely need the same limit.
- Calculate the request budget as instances multiplied by polls per hour, then add an allowance for retries.
- If the maximum age cannot be met inside the budget, change the distribution boundary (for example, one poller feeding a shared cache) or the vendor arrangement. Raising retry frequency does not resolve the conflict; it only adds requests to a budget that is already exceeded.
How should a client handle a 429 rate limit?
Treat a 429 as a budget signal, not as a transient failure to retry faster. The steps below are general client practice, not vendor-specified behavior, so confirm the provider’s quota scope and retry guidance before adopting them.
- Identify the quota scope: which key, project, environment or endpoint the limit applies to. A limit on one scope should not pause unrelated refreshes.
- Record the attempt before making any other decision: outcome set to rate_limited, the HTTP status class, and the current
attempt_number. - If the response includes
Retry-After, wait at least that long. The HTTP specification defines this header in both delay-seconds and HTTP-date forms, so check the current protocol text before hard-coding a parser for either form. - If no delay is supplied, back off exponentially with bounded jitter and a hard cap on the total delay.
- Keep serving the last-known-good snapshot throughout. Validate its schema before accepting new definitions, and never replace a valid snapshot with a partial or malformed response.
- When the cap is reached, stop retrying, raise one alert, and let the next scheduled poll try again.
To avoid one retry loop per process, route refreshes through a single owner per deployment boundary, such as a shared cache or one designated poller. Synchronized retries from many instances can keep a shared limit saturated, so jitter helps only when every instance applies it.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Instrument with OpenTelemetry
Initialize before your code loads
Initialize the Node SDK before application modules obtain tracers or meters. The OpenTelemetry Node SDK reference warns that late initialization can leave no-op implementations in place, which means the metrics you expect are never recorded. A common setup is a dedicated telemetry file loaded first:
node --require ./telemetry.js server.js
For ES module projects, load the same file with --import instead. OpenTelemetry’s JavaScript documentation lists traces and metrics as stable and supports active and maintenance LTS Node.js releases. Check its version guidance before pinning your runtime.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Best Value
Counters and histograms to keep
Counters accumulate totals, and histograms capture distributions such as latency. The instruments below cover the questions in this article:
flag_config_refresh_attempts(counter), labeled by provider and outcome, so that rate-limited and timed-out attempts stay visible.flag_evaluations(counter), labeled by provider, sdk_mode and a bounded cohort tier.flag_refresh_duration(histogram, in seconds).flag_snapshot_age(histogram, in seconds), recorded at evaluation time so stale evaluations are measurable.
const { metrics } = require('@opentelemetry/api');
const meter = metrics.getMeter('feature-flags');
const refreshAttempts = meter.createCounter('flag_config_refresh_attempts');
refreshAttempts.add(1, { provider: 'posthog', outcome: 'rate_limited' });
Keep cohort labels bounded
Each unique combination of attribute values requires aggregation state. The OpenTelemetry metrics documentation puts it directly: “The cardinality of a metric is the number of unique attribute combinations reported for it.” The default cardinality limit is 2,000 per metric stream, and the documentation says it can be overridden with a View. Once a stream reaches the limit, new measurements fold into an overflow point that drops their original attributes. A cohort-filtered query will then undercount, and the undercount is silent unless you check for it. Raising the limit increases aggregation state, so the cost moves rather than disappears.
In practice, give metrics a fixed set of cohort tiers rather than cohort IDs, and keep the tier set small enough that the product of all label combinations stays well below 2,000. Use the full cohort_id in logs and traces, and derive per-cohort counts from those records when a cohort-level question arises. Metrics answer how much and how often; the log records answer which cohort.
Validate the attribution pipeline
No single standard governs this reconciliation, so treat the checks below as a practical baseline rather than a vendor requirement. Run them before using the dashboard for chargeback or experiment decisions.
Recommended Free Tools
- Exporter totals match application attempt counters for the same window. A gap usually points to dropped telemetry or instrumentation that was never initialized.
- Provider usage reports match the request classes the provider documents as billable. Reconcile evaluations and definition polling separately.
- A sample of evaluations carries a cohort assignment and configuration version that match the rules actually in effect at that time.
- Refresh failures and retries line up with rising snapshot age and with stale-evaluation counts.
- No metric stream has reached its cardinality limit. If one has, its cohort-filtered totals are unreliable until the label set is reduced.
A failed check means the cohort numbers are not yet safe to use for decisions.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




