> ## Documentation Index
> Fetch the complete documentation index at: https://gccai.heqingsong.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Best Practices

> Polling patterns, prompt design, image guidance, error-retry strategies, concurrency, and troubleshooting tips for Midjourney integration

Consolidated best practices for common questions, performance tuning, and error handling. **Recommended reading before you integrate.**

## Task submission and polling

Submission endpoints are all asynchronous tasks: after submitting they return a `task_id`, then you periodically query `GET /v1/midjourney/{task_id}` for the status until `SUCCESS` / `FAILURE`.

```python theme={null}
import time, httpx

def wait_task(task_id, timeout=300):
    deadline = time.time() + timeout
    while time.time() < deadline:
        resp = httpx.get(f"{HOST}/v1/midjourney/{task_id}",
                         headers={"Authorization": f"Bearer {API_KEY}"}).json()
        if resp["status"] in ("SUCCESS", "FAILURE"):
            return resp
        if resp["status"] == "MODAL":
            raise RuntimeError(f"task {task_id} needs a /modal call to supply params and complete")
        time.sleep(3)
    raise TimeoutError(task_id)
```

* **Polling cadence**: 3–5s is recommended; higher frequency is pointless and wastes quota.
* **Do not block synchronously in a web request** waiting for the task to finish — return the `task_id` immediately after submitting and let the frontend poll asynchronously.

## Prompt design

**A good prompt:**

```text theme={null}
a serene mountain lake at sunrise, photorealistic, soft golden light,
mist rising from water, snow-capped peaks in distance --ar 16:9 --v 8.1 --s 100
```

* **Subject first**: lead with the subject, then describe the scene, and put modifiers last.
* **Make structured params explicit**: using `--ar` / `--v` / `--s` (or the corresponding body fields) is more controllable than relying on defaults.
* **Avoid ambiguous words**: `photorealistic` is clearer than `realistic`.

**Avoid:** being overly abstract ("make it good"), scattered subjects (multiple parallel objects with no clear priority), and quoting words (they are treated as literal values).

**Niji anime:** pass `niji: true` + `version: "7"`; the platform normalizes it to `--niji 7`, and billing goes through `midjourney@imagine-niji7`.

## Image-guidance best practices

| Source                      | Recommended approach                                               | Notes                                                                |
| --------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------- |
| User upload                 | Store it in your own OSS / CDN first, then pass that URL on submit | Do not pass base64 directly (wastes bandwidth)                       |
| Public URL                  | Pass it directly                                                   | Watch out for SSRF (must be publicly reachable) and the 12 MiB limit |
| Third-party / other outputs | Re-host to your own OSS first                                      | Third-party URLs may expire                                          |

* **Compress to \< 5 MiB**: the platform limit is 12 MiB, but smaller images transfer / process faster.
* PNG / JPG / WebP are all fine; high-quality JPG is recommended.
* A resolution of 1024–2048 px is already enough; higher is wasteful.
* Image weight `iw` (0–3, default 1): >1 stays closer to the source image, \<1 is more free.

## Error handling and retry strategy

| code                  | Meaning                                         | Retry strategy                                                                                  |
| --------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `1` / `200`           | Success                                         | ✅                                                                                               |
| `4` VALIDATION\_ERROR | Bad parameters                                  | ❌ Do not retry; fix the parameters                                                              |
| `3` NOT\_FOUND        | No available instance / task\_id does not exist | If the instance is unavailable you can retry later; do not retry if the task\_id does not exist |
| `9` FAILURE           | Service rejection / internal error              | ⏳ Retryable, exponential backoff (1s, 4s, 16s)                                                  |
| `21` MODAL            | Non-terminal state                              | ✅ Keep calling `/modal`                                                                         |
| `24` BANNED\_PROMPT   | Sensitive word                                  | ❌ Do not retry; change the prompt; **already auto-refunded**                                    |
| `429`                 | Rate limited                                    | ⏳ Exponential backoff + jitter                                                                  |
| `5xx` / network error | Server / network                                | ⏳ Exponential backoff; for network errors you may retry once immediately                        |

```python theme={null}
import time, random, httpx

def submit_with_retry(payload, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            r = httpx.post(f"{HOST}/v1/midjourney/generations/imagine",
                           json=payload,
                           headers={"Authorization": f"Bearer {API_KEY}"},
                           timeout=30)
            data = r.json()
            if r.status_code == 200 and data["code"] in (1, 200):
                return data
            if data["code"] in (4, 24):
                raise ValueError(data["description"])      # not retryable
            if data["code"] == 3 and "task" in data["description"]:
                raise ValueError(data["description"])      # task_id does not exist
            # the rest (9 / 429 / 5xx) are retryable
        except httpx.RequestError:
            pass
        time.sleep((4 ** attempt) + random.uniform(0, 1))  # 1s / 4s / 16s ...
    raise RuntimeError(f"reached max retry count {max_attempts}")
```

## Follow-up operation flow

```python theme={null}
# imagine → poll → upscale
imagine_id = submit({"prompt": "a cat"})["data"][0]["task_id"]
result = wait_task(imagine_id)           # grid_image_url + 4 image_urls + buttons
upscale_id = submit_to("/upscale", {"task_id": imagine_id, "index": 2})["data"][0]["task_id"]
final = wait_task(upscale_id)            # upscale is composed locally, 1–2s
single_image = final["image_urls"][0]
```

Inpaint (two-step inpaint → modal):

```python theme={null}
imagine_id = submit({"prompt": "a portrait"})["data"][0]["task_id"]; wait_task(imagine_id)
upscale_id = submit_to("/upscale", {"task_id": imagine_id, "index": 1})["data"][0]["task_id"]; wait_task(upscale_id)

inpaint_id = submit_to("/inpaint", {"task_id": upscale_id})["data"][0]["task_id"]  # status=modal
# Frontend draws the mask (white = repaint area), uploads it to your own OSS to get mask_url
final = submit_to("/modal", {
    "task_id": inpaint_id,
    "prompt": "replace the eyes with cybernetic blue eyes",
    "mask_url": "https://your-oss.com/mask.png"
})
wait_task(final["data"][0]["task_id"])
```

> ⚠️ After inpaint enters MODAL you must call `/modal` **within 30 minutes**, otherwise the backend auto-cancels (CANCEL) and refunds.

## Video billing control

* Single segment: `batch_size: 1` → charged 1 × `midjourney@video`
* Batch of 4 segments: `batch_size: 4` → charged 4 × `midjourney@video`
* HD single segment: `video_type: "vid_1.1_i2v_720"` + `batch_size: 1` → charged 1 × `midjourney@video-720p`

**Recommendation**: if you only need 1 segment for delivery, use `batch_size=1`; only use 4 for batch comparison drafts. Do not default to 4 (it multiplies cost N times).

## Concurrency and throughput

```python theme={null}
import asyncio
sem = asyncio.Semaphore(10)  # client submits at most 10 concurrently

async def submit_one(prompt):
    async with sem:
        return await submit({"prompt": prompt})
```

* The platform has a per-minute submission cap; exceeding it returns `429`, which needs backoff retry.
* Actual generation concurrency is determined by system capacity; exceeding it queues; a task staying in `SUBMITTED` for a long time usually means it is queued.
* Always include `sleep` when polling; do not spin in a tight loop without sleep.

## Monitoring recommendations

| Metric                         | Reference threshold | Meaning                                                         |
| ------------------------------ | ------------------- | --------------------------------------------------------------- |
| Task SUCCESS rate (last 1h)    | > 95%               | Low values indicate service / network issues                    |
| Average completion time        | \< 90s              | High values indicate queuing                                    |
| Number of tasks stuck in MODAL | Near 0              | Many indicate the client did not call `/modal`                  |
| Proportion of `code=24`        | \< 5%               | High values indicate prompts frequently trigger sensitive words |

## Troubleshooting checklist

| Symptom                                   | Where to look                                                                    |
| ----------------------------------------- | -------------------------------------------------------------------------------- |
| Task stuck in `SUBMITTED` for a long time | Queued in the system; check again later                                          |
| Task stuck in `NOT_START` for a long time | The platform will auto-timeout and refund later; no manual action needed         |
| Task in `MODAL` over 30 minutes           | The client did not call `/modal`; it has been auto-cancelled (CANCEL) + refunded |
| `prompt` field is empty                   | The text result of a describe task is in the `description` field                 |
| Missing one image in `image_urls`         | Content moderation blocked part of the images; check `fail_reason`               |
| Billing higher than expected              | Check the `quota` field; for video remember to multiply by `batch_size`          |
