neondatabase/neon · error

Failed to get feature flags: {}, {}

Error message

Failed to get feature flags: {}, {}

What it means

get_feature_flags_local_evaluation_raw issues a GET with the server_api_key as a bearer token to either /api/feature_flag/local_evaluation (new secure key) or /api/projects/{project_id}/feature_flags/local_evaluation (legacy personal token). If PostHog answers with a non-2xx status, the error embeds both the HTTP status and the response body, which usually states the exact reason (invalid key, unknown project, plan limits).

Source

Thrown at libs/posthog_client_lite/src/lib.rs:577

            )
        } else {
            // The old personal API token
            format!(
                "{}/api/projects/{}/feature_flags/local_evaluation",
                self.config.private_api_url, self.config.project_id
            )
        };
        let response = self
            .client
            .get(url)
            .bearer_auth(&self.config.server_api_key)
            .send()
            .await?;
        let status = response.status();
        let body = response.text().await?;
        if !status.is_success() {
            return Err(anyhow::anyhow!(
                "Failed to get feature flags: {}, {}",
                status,
                body
            ));
        }
        Ok(body)
    }

    /// Fetch the feature flag specs from the server.
    ///
    /// This is unfortunately an undocumented API at:
    /// - <https://posthog.com/docs/api/feature-flags#get-api-projects-project_id-feature_flags-local_evaluation>
    /// - <https://posthog.com/docs/feature-flags/local-evaluation>
    ///
    /// The handling logic in [`FeatureStore`] mostly follows the Python API implementation.
    /// See `_compute_flag_locally` in <https://github.com/PostHog/posthog-python/blob/master/posthog/client.py>
    pub async fn get_feature_flags_local_evaluation(
        &self,
    ) -> Result<LocalEvaluationResponse, anyhow::Error> {

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Read the embedded response body first -- it distinguishes bad key (401), bad project (404), and plan/limit errors
  2. Verify server_api_key is a PostHog personal/secure API token with read access to the project, and project_id matches it
  3. Confirm private_api_url is the private/API base URL of your PostHog instance, not the public capture URL
  4. For 5xx/timeouts, retry with backoff (transient PostHog or network issues); for 4xx fix credentials/configuration instead

Example fix

# before
POSTHOG_API_URL='https://eu.i.posthog.com'        # public capture URL used as private API
POSTHOG_API_KEY='phx_...'                          # client key used as server key

# after
POSTHOG_API_URL='https://eu.i.posthog.com'          # same host is fine for cloud
POSTHOG_SERVER_API_KEY='phx_team_personal_token'    # personal/secure token w/ read scope
Defensive patterns

Strategy: retry

Validate before calling

// Cheap pre-flight: distinguish credential errors (do not retry) from transient ones.
async fn fetch_feature_flags_guarded(client: &PosthogClientLite) -> anyhow::Result<String> {
    match client.get_feature_flags_local_evaluation_raw().await {
        Err(e) => {
            let msg = format!("{e:#}");
            if msg.contains(" 401, ") || msg.contains(" 403, ") || msg.contains(" 404, ") {
                anyhow::bail!("posthog credentials/project misconfigured (fatal): {msg}");
            }
            Err(e) // 5xx / timeouts: retriable by caller
        }
        ok => ok,
    }
}

Try / catch

use backoff::{backoff::Backoff, ExponentialBackoff};

let mut bo = ExponentialBackoff::default(); // default max elapsed ~15m, jittered
loop {
    match client.get_feature_flags_local_evaluation_raw().await {
        Ok(body) => break Ok(body),
        Err(e) => {
            let msg = format!("{e:#}");
            let fatal = msg.contains(" 401, ") || msg.contains(" 403, ") || msg.contains(" 404, ");
            if fatal || bo.next_backoff().is_none() {
                break Err(anyhow::anyhow!("feature flag fetch failed: {msg}"));
            }
            tracing::warn!("retrying posthog feature flags after: {msg}");
        }
    }
}

Prevention

When it happens

Trigger: 401/403 with a wrong or expired server_api_key; 404 from a wrong project_id or wrong private_api_url base; 402 or plan-related errors when the PostHog plan lacks local evaluation; 5xx during a PostHog outage -- each surfaces as 'Failed to get feature flags: <status>, <body>'.

Common situations: Rotated personal API keys not updated in config; mixing up client_api_key (public) and server_api_key (private); pointing private_api_url at the public capture endpoint; self-hosted PostHog behind an auth-stripping proxy; egress blocked by firewall producing proxied 4xx/5xx responses.

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/414ba0adfd8a845d. Report an issue: GitHub.