neondatabase/neon · warning · ApiError

duration must be 1-60 secs

Error message

duration must be 1-60 secs

What it means

Returned as HTTP 400 BadRequest by profile_cpu_handler in neon's http-utils when the seconds query parameter of /profile/cpu is present but outside the accepted 1..=60 range (parse failures produce a different 'cannot parse query param' error). The bounds cap how long the profiler may block the debugging endpoint; the default is 5 seconds.

Source

Thrown at libs/http-utils/src/endpoint.rs:384

/// Generates CPU profiles.
pub async fn profile_cpu_handler(req: Request<Body>) -> Result<Response<Body>, ApiError> {
    enum Format {
        Pprof,
        Svg,
    }

    // Parameters.
    let format = match get_query_param(&req, "format")?.as_deref() {
        None => Format::Pprof,
        Some("pprof") => Format::Pprof,
        Some("svg") => Format::Svg,
        Some(format) => return Err(ApiError::BadRequest(anyhow!("invalid format {format}"))),
    };
    let seconds = match parse_query_param(&req, "seconds")? {
        None => 5,
        Some(seconds @ 1..=60) => seconds,
        Some(_) => return Err(ApiError::BadRequest(anyhow!("duration must be 1-60 secs"))),
    };
    let frequency_hz = match parse_query_param(&req, "frequency")? {
        None => 99,
        Some(1001..) => return Err(ApiError::BadRequest(anyhow!("frequency must be <=1000 Hz"))),
        Some(frequency) => frequency,
    };
    let force: bool = parse_query_param(&req, "force")?.unwrap_or_default();

    // Take the profile.
    static PROFILE_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
    static PROFILE_CANCEL: Lazy<Notify> = Lazy::new(Notify::new);

    let report = {
        // Only allow one profiler at a time. If force is true, cancel a running profile (e.g. a
        // Grafana continuous profile). We use a try_lock() loop when cancelling instead of waiting
        // for a lock(), to avoid races where the notify isn't currently awaited.
        let _lock = loop {
            match PROFILE_LOCK.try_lock() {

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Set seconds to a value between 1 and 60 inclusive, e.g. ?seconds=30
  2. For longer coverage, take consecutive 60s profiles and merge them offline rather than one long request
  3. Check scrape config (pyroscope/alloy scrape interval) and clamp it to <=60s for this endpoint

Example fix

# before
curl 'http://localhost:9898/profile/cpu?seconds=300'   # 400 duration must be 1-60 secs

# after
curl 'http://localhost:9898/profile/cpu?seconds=60'
Defensive patterns

Strategy: validation

Validate before calling

const seconds = Math.min(Math.max(Number(process.env.PROFILE_SECONDS ?? 5), 1), 60);
await fetch(`/profile/cpu?seconds=${seconds}`);

Type guard

function isValidProfileSeconds(s) { const n = Number(s); return Number.isInteger(n) && n >= 1 && n <= 60; }

Prevention

When it happens

Trigger: GET /profile/cpu?seconds=0, ?seconds=61, or ?seconds=120 — any integer below 1 or above 60 returns this 400.

Common situations: Grafana continuous-profiling scrape configured with a 30s+ interval that drifts over 60; someone requesting a long profile to capture a rare event; default 15m/interval arithmetic producing out-of-range seconds.

Related errors


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