Skip to content

StudioNetwork Conditions

Network Conditions

Make a mock answer as a slow or failing network would and take uploads as a strict server does, from Studio's Network tab or in code, and halt a run on its own.

15 min readboth modes

A mock can answer as a slow or unreliable network would, and take uploads as a server with limits does. It adds latency to its answers, caps how fast it sends them and how fast uploads arrive, refuses an upload over a size or of a type it does not take, and fails a share of calls with any HTTP error status, a timeout, a dropped connection or an upload cut off partway. Set the conditions in Studio's Network tab while the mock runs, or declare them with the mock in code. Stop conditions end a run on their own once it fails too often, grows too slow, or has run long enough.

What they apply to

  • Every HTTP call the mock answers: at its routes, at the proxy route /api/request and by name. That includes the calls it passes to the source's own server and the ones it answers from recordings (see Mock only some endpoints, or answer with recordings).
  • Every request body the mock reads, at every path: the upload speed paces it, and the size limit and types refuse an upload before the handler runs.
  • A stream: its faults, and its latency, which comes before the stream opens. A bandwidth cap does not slow its events.
  • Not a channel: WebSocket subscriptions are answered as they would be with no conditions set.
  • Whether a call fails is decided as it arrives. Its body is read first — at the upload speed, and refused if the conditions refuse the upload — and the fault answers it then; an interrupt cuts the body off halfway instead. A failed call gets no added latency; a timeout fault holds it instead.
  • An endpoint mocked with a delay of its own, by .delay(ms), .withLatency(...) or a sequence entry's delay, waits that delay instead of the network's latency. Its faults still apply.

The Network tab

Open a running mock in Studio's Mocks view and pick Network. A change goes to the mock at once. While the mock is stopped, the tab takes none.

Network condition

The card at the top names what the mock simulates: its chaos mode, its delay as a base ± jitter, its download cap, how it takes uploads, and how many routes an override of their own delays, fails or limits. Stop ends all of it. It clears the chaos, the delay, the cap and the upload conditions, and switches every route override off, keeping what each one sets. The banner over the Mocks view offers it too, as Stop chaos, while a chaos mode is on.

Beside it are four live figures, each with a sparkline of the last minute:

  • Throughput: the answers in the last minute.
  • Error rate: the share of the latest 60 answers that failed.
  • P50 and P99: the median and 99th percentile of the latest 60 answers' durations, shown in amber past Studio's Slow request threshold (in Settings) and in red past twice that.

Chaos modes

ModeFailsWith
Stablenothingreal handlers only
Degraded10% of answers500 (60%), timeout (40%)
Unstable40%500 (40%), 503 (30%), timeout (20%), reset (10%)
Downevery answer503
Customyour own rateyour own mix

Custom starts from the rate and mix in force, or 30% split 50/30/20 between 500, 503 and timeout when nothing fails yet.

Fault injection

Error rate sets the share of answers the mock fails, from 0 to 100%. The Fault mix lists the faults those failures take and weighs them against each other. A weight is relative, and the percentage beside it is its share of the failures. Add a fault adds any fault from the list below, at a weight of 20; the cross beside a fault takes it out of the mix, down to the last one. Moving the rate or a weight, or changing the mix, makes the run Custom. A mix changed while nothing fails is kept for when a rate is set; Stable clears it.

FaultThe call gets
HTTP 500, 502, 504, 507that status at once
HTTP 503a 503 at once, with Retry-After: 5
HTTP 400, 403, 404, 409, 413, 415, 422that status at once
HTTP 401a 401 at once, with WWW-Authenticate: Bearer realm="mock" — a challenge no browser answers with a sign-in prompt
HTTP 429a 429 at once, with Retry-After: 5
timeoutheld until it times out, then a 408
resetits connection dropped, with no answer
interruptits connection dropped once half its body has arrived, so an upload fails partway. A call with no body is dropped as a reset drops it.

A status is answered with { "error": "HTTP 413 Content Too Large (simulated)" }: the status, its reason, and the word Studio tells an injected fault from a handler's own error by.

Latency

PresetDelays
Nonenone
3G300–900 ms, ± 150 ms
4G100–350 ms, ± 60 ms
Satellite600–800 ms, ± 150 ms
Customyour own

A preset draws its delays log-normally: most of them short, now and then one near the top of its window. The card shows a delay as Base delay ± Jitter, and its window on a track from 0 to 2 s. 3G reads 600 ms ± 450 ms, from 150 ms to 1.05 s.

With Custom, the base (up to 2000 ms) and the jitter (up to the base, and at most 1000 ms) set a window from the base less the jitter to the base plus it. Shape sets how delays spread across that window:

  • Uniform: every delay in the window is as likely. This is the default.
  • Normal: most delays near the base, few at the edges.
  • Log-normal: most delays short, now and then one near the top, as on a real network.

A base of 0 adds no delay. Download speed caps how fast answers are sent: Unlimited, 512 Kbps, 1.5 Mbps or 10 Mbps. An answer then waits as long as its JSON body takes to send at that rate, on top of its delay. How fast uploads arrive is the Uploads card's.

Uploads

The Uploads card sets how the mock takes request bodies. Each setting goes to the mock as it is picked, and one the mocks file set shows among the presets.

SettingWhat it does
Upload speed: Unlimited, 512 Kbps, 1.5 Mbps or 10 MbpsThe mock reads every request body no faster, a form, a file or JSON alike, so the app's upload progress moves as it would on that link: a 4 MB file at 1.5 Mbps takes about 22 s.
Size limit: None, 100 KB, 1 MB or 5 MBAn upload over it, a form or a file, is answered 413 before it is read: { "error": "The upload is 3.2 MB, over the 1 MB the mock takes" }. JSON keeps the mock's own limit for JSON, and no limit here raises maxUploadBytes, the most the mock reads.
File typesTypes as <input accept> lists them: a type (application/pdf), a family (image/*) or an extension (.csv), separated by commas. An upload holding a file of another type is answered 415: { "error": "notes.txt (text/plain) is not a type the mock takes: image/*, .pdf" }. Empty takes any type.

The operating system takes the first part of an upload into its buffers at once, a couple of megabytes over localhost, so the app's progress runs that far ahead of what the mock has read, and the pace shows on a larger file. The mock's own log shows what it has read: a slow upload is in the Logs list as it arrives, its code ↑42%, and its request sheet says it is uploading until the body is whole.

Per-route overrides

The card lists every route the mock serves, each with a switch. Open a route to give it settings of its own:

  • Delay: 0–2000 ms, fixed.
  • Error rate: 0–100%, in steps of 5.
  • Fails with: the faults its failures take in place of the mix, picked from the same list. Faults picked for a route that nothing would fail, with no rate of its own and a global rate of 0, come with an error rate of 100%.
  • Size limit: 100 KB, 1 MB or 5 MB, or Global.
  • File types: typed as on the Uploads card; empty keeps the global ones, and */* takes any type on this route.

A setting the route has none of its own for reads global, and a slider rests at the global value.

  • Setting one switches the override on. The route then takes that setting in place of the global one and keeps the others: a route given only a delay still fails as often as the rest, Down included.
  • A route set to 0% never fails while the mock fails the others, Down included. Its row reads never fails, the editor says what the rest of the mock fails, and a line under the chaos modes names every route with an error rate of its own, so a route that keeps answering, or fails while the mock is Stable, is never a surprise.
  • The download cap and the upload speed are always the global ones.
  • The reset beside a delay or rate takes it back to the global one, which its tooltip names; so do Global, an emptied types field and taking out the last fault. The override switches off once it sets nothing of its own. Use the global settings clears the override and switches it off.
  • Switching a route off keeps what it sets, so it can be switched back on as it was. Switching on a route with nothing of its own opens it, so you can set something.

The mock holds every route's override, switched on or off, and Studio sends it one route at a time: every page shows the same overrides, a change made on another page to another route stays, and a reload shows them as they were.

The tag on each row sums up what the route sets of its own: +300ms, 50%, never fails, 100% · 413, 429, ≤ 1 MB · image/*, or global.

Safety

Auto-halt ends a run on its own once a threshold is crossed:

ThresholdEnds the run whenDefault
Error rate abovethe share of failed answers over the last 30 s reaches it, once those 30 s hold 5 answers30%
P99 abovethe p99 of the durations over the last 60 s reaches it, once those 60 s hold 10 answers2000 ms
Run at mostthe run has lasted that many minutes, on time even if no call comesno limit

A threshold set to 0 is off. Auto-halt itself is off until switched on, and the thresholds apply to any run, chaos or delay. A run starts when the mock begins to simulate anything, and lasts until it simulates nothing. A halt clears what the mock simulated, its chaos, its delay, its cap and its upload conditions, switches every route override off, keeping what each one sets, as Stop does, and keeps the thresholds for the next run.

Runs lists each start, stop and halt, newest first: what ran, when, how long it lasted, and which threshold ended a halted run.

Observed

The Observed card shows the latest 60 answers:

  • Latency distribution: how their durations spread, from under 50 ms to over 4 s, coloured against the slow threshold.
  • Status timeline: one cell per answer, oldest first. A cell opens its request.
  • Injected faults by type: how many answers each fault in the mix failed, and any other fault the latest answers met, a route's own, say. A handler's own errors are not counted.

When the mock starts again

Studio keeps what the tab sets, the route overrides switched off included, and the scenario applied, in the project's database, .courier/studio.db. A mock that starts again, after Restart, a crash, a restart of Studio, or an edit of courier.mocks.ts that leaves the conditions it declares as they were, gets them back. An edit that declares other conditions is the newer word: the mock starts with those, and Studio forgets what it kept for it.

In code

network sets the conditions a mock starts with:

  • defineMockServer({ network }) in courier.mocks.ts, for a mock Studio runs.
  • createMockServer({ network }) from @noverax/courier-mock/server, for a mock server of your own.
  • createMockClient({ network }) from @noverax/courier-mock/testing, for a mock client in tests.
ts
// src/api.ts
import { courier, defineApi, defineGroup } from "@noverax/courier/define"

type User = { id: number; name: string }

const { endpoint } = courier()
const users = defineGroup({
  prefix: "users",
  endpoints: {
    getAll: endpoint.get("").response<User[]>(),
    getById: endpoint.get(":id").variables<{ id: number }>().response<User>(),
    uploadAvatar: endpoint.post("avatar").variables<FormData>().response<User>(),
  },
})
export const api = defineApi({ baseURL: "https://api.example.com", groups: { users } })
ts
// courier.mocks.ts
import { defineMocks, defineMockServer } from "@noverax/courier-mock"
import { api } from "./src/api"

export default defineMocks({
  api: defineMockServer({
    source: api,
    network: {
      latency: "4g",
      errorRate: 0.1,
      faultWeights: [
        { type: "503", weight: 3 },
        { type: "timeout", weight: 1 },
      ],
      timeout: 5000,
      uploads: { bandwidth: 1500, maxBytes: 5 * 1024 * 1024 },
      routeOverrides: [
        { routeId: "users:getById", enabled: true, errorRate: 0.5 },
        {
          routeId: "users:uploadAvatar",
          enabled: true,
          faultTypes: ["413", "interrupt"],
          uploads: { maxBytes: 1024 * 1024, accept: ["image/png", "image/jpeg"] },
        },
      ],
      stopConditions: {
        enabled: true,
        errorRateThreshold: 50,
        errorRateDurationSecs: 30,
        p99Threshold: 0,
        p99DurationSecs: 60,
        maxDurationMins: 10,
      },
    },
  }),
})
FieldWhat it sets
latencyThe delay before each answer: a number of ms, a profile, or a range { min, max, jitter } in ms. The profiles are "3g" (300–900 ms, ± 150), "4g" (100–350, ± 60), "wifi" (15–60, ± 15) and "fiber" (1–8, ± 2).
distributionHow a delay is drawn between min and max: "uniform" (the default), "normal" or "log-normal". The jitter is added either way, and no delay goes below 0.
bandwidthA cap in kilobits a second. An answer waits as long as its JSON body takes to send at this rate, on top of the latency. 0 or unset is unlimited.
uploads{ bandwidth?, maxBytes?, accept? }: how the mock takes request bodies. bandwidth is the speed in kilobits a second the mock reads every body at, a form, a file or JSON alike. maxBytes is the largest form or file it takes; a larger one is answered 413 before it is read, and it cannot raise maxUploadBytes. accept lists the types of file it takes, as <input accept> does; an upload holding another is answered 415. Each left out is none.
errorRateThe share of calls that fail, from 0 (none) to 1 (every one).
faultTypesThe faults a failing call takes, each as likely: any error status from "400" to "599", "timeout", "reset" and "interrupt". Unset, 500 and 503.
faultWeights{ type, weight } pairs, the weights relative to each other. When set, they are used in place of faultTypes.
timeoutHow long a timeout fault holds a call, in ms. A mock server holds it 30 000 ms unless set, or until the client gives up. A mock client waits this long, and fails at once unless set.
routeOverrides{ routeId, enabled, latency?, errorRate?, faultTypes?, uploads? } for one route, named "group:endpoint". Each field an enabled override sets is used for that route in place of the global one; a field it leaves unset stays the global one, and an errorRate of 0 keeps the route from failing whatever the global rate. Faults it names replace the global mix, and its uploads take maxBytes and accept, each in place of the global one; the upload speed stays the global one. An override with enabled: false keeps its settings and is not used.
stopConditions{ enabled, errorRateThreshold, errorRateDurationSecs, p99Threshold, p99DurationSecs, maxDurationMins }, every field given: thresholds in percent and ms, windows in seconds, the longest run in minutes. A threshold of 0 is off.

How each fault fails a call:

FaultA mock serverA mock client
A status, "400" to "599"answers it at once, with { "error": "HTTP 503 Service Unavailable (simulated)" } and the headers a server sends with it: WWW-Authenticate: Bearer realm="mock" with a 401, Retry-After: 5 with a 429 or a 503. A status outside 400–599 is answered 500.throws an HTTPError with that status, the same body as its data, and the same headers
"timeout"holds the call for timeout, or until the client gives up, then answers 408waits timeout if set, then throws a 408
"reset"drops the connection without an answer, or answers 502 to a call made with app.request()throws a 502
"interrupt"drops the connection once half the call's body has arrived, or at once for a call with no body; answers 502 to a call made with app.request()throws a 502 before the handler runs

An upload the conditions refuse is answered as a server with limits answers it:

UploadA mock serverA mock client
over uploads.maxBytes413 before the body is read, { "error": "The upload is 3.2 MB, over the 1 MB the mock takes" }, the rest of the body taken off the connection so a browser still sending it hears the answerthrows an HTTPError 413 with the same body, measuring a form as it would travel, encoded
holding a file uploads.accept does not list415, { "error": "notes.txt (text/plain) is not a type the mock takes: image/*" }throws an HTTPError 415 with the same body

A mock client applies faults, latency, its distribution, route overrides and the upload size limit and types to its HTTP calls. It has no wire, so neither bandwidth nor uploads.bandwidth applies, and neither do stop conditions, which halt a mock server's runs. Its streams and channels answer as they would with no conditions set.

Change them while the server runs

A mock server from createMockServer holds its conditions in server.networkState. mergeConfig changes the fields it names, setConfig replaces them all, and setConfig(undefined) clears them. onHalt hears when a stop condition ends a run, with the reason and how long the run lasted in ms. The run's settings are cleared by then, its route overrides switched off with their settings, and its stop conditions kept.

ts
// mock-server.ts
import { createMockServer } from "@noverax/courier-mock/server"

const server = createMockServer({ port: 7002, network: { latency: { min: 50, max: 150 } } })
server.mock("users", "getAll").resolves([])
server.networkState.onHalt = ({ reason, durationMs }) => {
  console.log(`Halted after ${durationMs} ms: ${reason}`)
}
await server.start()

server.networkState.mergeConfig({ errorRate: 0.2, faultTypes: ["503"] })