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).

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.

Appendix: 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)