nexu-io/open-design · error · Error

leonardo.ai generation timed out after 2 minutes

Error message

leonardo.ai generation timed out after 2 minutes

What it means

Thrown after the Leonardo polling loop exits (startedAt + maxPollMs = 120000ms = 2 minutes) without imageUrl being set. This is the poll-ceiling timeout: the job neither reached COMPLETE nor FAILED inside the 2-minute window. The message hard-codes '2 minutes' because maxPollMs is a constant.

Source

Thrown at apps/daemon/src/media/index.ts:2283

      throw new Error(`leonardo.ai poll ${pollResp.status}`);
    }
    
    const pollData = (await pollResp.json()) as Record<string, any>;
    const generation = pollData?.generations_by_pk;
    
    if (generation?.status === 'COMPLETE') {
      const images = generation?.generated_images;
      if (Array.isArray(images) && images.length > 0) {
        imageUrl = images[0]?.url;
        break;
      }
    } else if (generation?.status === 'FAILED') {
      throw new Error('leonardo.ai generation failed');
    }
  }
  
  if (!imageUrl) {
    throw new Error('leonardo.ai generation timed out after 2 minutes');
  }
  
  // Fetch the generated image
  const imgResp = await fetch(imageUrl, withMediaRequestInit(ctx));
  if (!imgResp.ok) {
    throw new Error(`leonardo.ai image fetch ${imgResp.status}`);
  }
  
  const bytes = Buffer.from(await imgResp.arrayBuffer());
  
  return {
    bytes,
    providerNote: `leonardo.ai/${ctx.model} · ${ctx.aspect} · ${bytes.length} bytes`,
    suggestedExt: sniffImageExt(bytes),
  };
}

View on GitHub (pinned to 5be4028344)

Solutions

  1. Retry — slow queue days often resolve on resubmit.
  2. Reduce requested complexity (smaller dimensions like 1:1 1024², simpler prompt) so the job finishes inside 2 minutes.
  3. If your jobs consistently need longer, raise the maxPollMs constant (currently 120000) in apps/daemon/src/media/index.ts around line 2250 — consider making it env-configurable like the OpenRouter/Grok ceilings.
  4. If status was returning COMPLETE but imageUrl stayed null (images array empty), that's a different bug — capture pollData to inspect generations_by_pk.generated_images.

Example fix

// before
const maxPollMs = 120000; // 2 minutes

// after — env-configurable ceiling matching the OpenRouter/Grok pattern
const maxPollMs = Number(process.env.OD_LEONARDO_MAX_POLL_MS) || 120000;
Defensive patterns

Strategy: validation

Validate before calling

// Make the Leonardo poll ceiling configurable like OpenRouter/Grok so long jobs
// don't always time out at the 2-minute hard cap.
const maxPollMs = Number(process.env.OD_LEONARDO_MAX_POLL_MS) || 120000;

Try / catch

// Distinguish three post-loop states: status stuck on PENDING (queue backlog),
// status stuck on a value we don't recognize (parser drift), or COMPLETE with
// no images (upstream bug). Each needs a different operator action.
if (!imageUrl) {
  if (lastStatus === 'COMPLETE') throw new Error('leonardo.ai COMPLETE but no images — upstream bug, file a ticket');
  throw new Error(`leonardo.ai generation timed out after 2 minutes (last status: ${lastStatus || 'pending'})`);
}

Prevention

When it happens

Trigger: Loop iterates every 2000ms for at most 120000ms; if generation.status never becomes 'COMPLETE' (and never 'FAILED'), imageUrl stays null and the post-loop guard throws. Happens on slow Leonardo jobs that take >2min or when status gets stuck on PENDING.

Common situations: Busy Leonardo queue making jobs run >2min; complex prompt or large size that legitimately needs >2min; status stuck because the poll response shape changed and the COMPLETE branch never matches; maxPollMs is a constant so long jobs always time out.

Understand the failure class

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/8334ecd8508fa62e. Report an issue: GitHub.