k6 2.3 --once and --scenario: smoke-test the Sume API without spend

Use k6 --once and --scenario for a Sume API smoke test that costs nothing: models, a deliberate 400, and a paced balance read that counts 429s.

5 min readSume
All posts

Write one k6 script with two named scenarios, then run k6 run --once --scenario smoke sume.js for a single-pass check and k6 run --scenario pace sume.js to watch rate limiting. Both stay on free routes: the model list, a request that Sume rejects with a 400, and the balance read. No video job is created, so no credits are reserved.

k6 v2.3.0 added both flags. The release notes say --scenario selects named scenarios without editing the script, and --once runs a script one time while keeping the scenario's own function. A v1.8.2 security patch followed on 2026-10-12. I ran everything below on k6 v2.3.0 against a local stand-in for the API.

What belongs in a Sume smoke test?

Three cheap facts that fail loudly when a deploy or a key is wrong. The model list is GET /v1/videos/models, and it should include the id your code pins. A request with size must come back 400 unsupported_parameter, because the Video Generation guide says every model reports supported_sizes: null and the API rejects the field. That proves auth, routing and validation without a generation. The balance read, GET /v1/balance, returns data.balance.available_credits per the usage docs.

What each free call proves (read 2026-10-13)
CallExpectedProves
GET /v1/videos/models200, pinned id in dataKey works, model not retired
POST /v1/videos with size400 unsupported_parameterValidation runs before any job
GET /v1/balance200 or 429Reads work, limits are visible

How do you write the script?

Declare the scenarios and thresholds first. Each scenario names an exported function with exec.

import http from "k6/http";
import { check } from "k6";
import { Counter } from "k6/metrics";

const BASE = __ENV.SUME_BASE || "https://api.sume.com";
const auth = { headers: { Authorization: `Bearer ${__ENV.SUME_API_KEY}`, "Content-Type": "application/json" } };
const limited = new Counter("sume_429");

export const options = {
  scenarios: {
    smoke: { executor: "shared-iterations", vus: 1, iterations: 1, exec: "smoke" },
    pace: { executor: "constant-arrival-rate", rate: 5, timeUnit: "1s", duration: "20s", preAllocatedVUs: 5, exec: "pace" },
  },
  thresholds: { checks: ["rate==1"], http_req_failed: ["rate==0"] },
};

The smoke function has one trap. k6 counts any 4xx as a failed request by default, so the deliberate 400 would trip a http_req_failed threshold. Pass responseCallback: http.expectedStatuses(400) on that one request; in a run without it, the failure rate was 33% and the threshold failed.

export function smoke() {
  const models = http.get(`${BASE}/v1/videos/models`, auth);
  check(models, { "models 200": (r) => r.status === 200, "wan listed": (r) => r.json("data").some((m) => m.id === "wan-3.0") });
  const bad = http.post(`${BASE}/v1/videos`, JSON.stringify({ model: "wan-3.0", prompt: "x", size: "1280x720" }),
    { ...auth, responseCallback: http.expectedStatuses(400) });
  check(bad, { "size is a 400": (r) => r.status === 400 && r.json("error.code") === "unsupported_parameter" });
}

How do you watch rate limits without breaking the run?

The errors page says responses can include ratelimit-limit, ratelimit-remaining, ratelimit-reset and retry-after, and that a 429 means back off. It does not publish a request rate, so start low and raise it. The pace scenario treats 200 and 429 as expected, counts the 429s in a custom metric, and checks that every limited reply carries retry-after:

export function pace() {
  const r = http.get(`${BASE}/v1/balance`, { ...auth, responseCallback: http.expectedStatuses(200, 429) });
  if (r.status === 429) limited.add(1);
  check(r, { "200 or 429 with retry-after": (x) => x.status === 200 || x.headers["Retry-After"] !== undefined });
}

Run it with SUME_API_KEY=... k6 run --scenario pace sume.js. A rising sume_429 count tells you where your workspace's free-read ceiling sits; pace production polling below it.

What does --once refuse to do?

--once runs a single scenario. With both scenarios in the file, k6 run --once sume.js stops with the error --once can run only with a single scenario. Add --scenario smoke and it runs the smoke function once. Keep load scenarios away from POST /v1/videos: each accepted job reserves credits. Test the submit path with the validation 400 here, and with a single real job in CI.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume