cruel

core api

the base cruel api for fetch, services, and async functions

quick start

import { cruel } from "cruel"

const api = cruel(fetch, {
  fail: 0.1,
  delay: [100, 500],
  timeout: 0.05,
})

const res = await api("https://api.example.com")

cruel(fn, options) injects chaos into async functions.

try without api keys

cd packages/examples
bun run run.ts core

this runs local core examples only (core/basic.ts, core/resilience.ts, core/control.ts).

chaos options

type ChaosOptions = {
  fail?: number
  delay?: number | [number, number]
  timeout?: number
  jitter?: number
  corrupt?: number
  spike?: number | [number, number]
  enabled?: boolean
}
  • fail: chance to throw CruelError
  • delay: fixed or random latency
  • timeout: chance to never resolve
  • jitter: extra random latency between 0 and jitter
  • corrupt: chance to corrupt string output
  • spike: occasional added latency
  • enabled: force-disable on a wrapper

shortcut wrappers

cruel.fail(fn, 0.1)
cruel.slow(fn, [100, 500])
cruel.timeout(fn, 0.05)
cruel.flaky(fn)
cruel.unreliable(fn)
cruel.nightmare(fn)

domains

network

cruel.network.latency(fn, [100, 500])
cruel.network.packetLoss(fn, 0.1)
cruel.network.disconnect(fn, 0.05)
cruel.network.dns(fn, 0.02)
cruel.network.bandwidth(fn, 256)
cruel.network.slow(fn)
cruel.network.unstable(fn)
cruel.network.offline(fn)

http

cruel.http.status(fn, 500, 0.1)
cruel.http.status(fn, [500, 502, 503])
cruel.http.rateLimit(fn, { rate: 0.1, retryAfter: 60 })
cruel.http.serverError(fn, 0.1)
cruel.http.clientError(fn, 0.1)
cruel.http.badGateway(fn)
cruel.http.serviceUnavailable(fn)
cruel.http.gatewayTimeout(fn)

stream

cruel.stream.cut(fn, 0.1)
cruel.stream.pause(fn, [200, 1000])
cruel.stream.corrupt(fn, 0.1)
cruel.stream.truncate(fn, 0.1)
cruel.stream.reorder(fn, 0.1)
cruel.stream.duplicate(fn, 0.1)
cruel.stream.dropChunks(fn, 0.1)
cruel.stream.corruptChunks(fn, 0.1)
cruel.stream.slow(fn)
cruel.stream.flaky(fn)

ai

cruel.ai.rateLimit(fn, 0.1)
cruel.ai.overloaded(fn, 0.05)
cruel.ai.contextLength(fn, 0.02)
cruel.ai.contentFilter(fn, 0.01)
cruel.ai.modelUnavailable(fn, 0.02)
cruel.ai.slowTokens(fn, [50, 200])
cruel.ai.streamCut(fn, 0.1)
cruel.ai.partialResponse(fn, 0.1)
cruel.ai.invalidJson(fn, 0.05)
cruel.ai.realistic(fn)
cruel.ai.nightmare(fn)

state and control

cruel.enable({ fail: 0.1, delay: [100, 500] })
cruel.disable()
cruel.toggle()
cruel.isEnabled()

await cruel.scope(async () => {
  await api("https://api.example.com")
}, { fail: 0.2 })

resilience wrappers

cruel.circuitBreaker(fn, { threshold: 5, timeout: 30000 })
cruel.retry(fn, { attempts: 3, delay: 1000, backoff: "exponential" })
cruel.bulkhead(fn, { maxConcurrent: 10, maxQueue: 100 })
cruel.withTimeout(fn, { ms: 5000 })
cruel.fallback(fn, { fallback: backup })
cruel.hedge(fn, { count: 2, delay: 100 })
cruel.rateLimiter(fn, { requests: 100, interval: 60000 })
cruel.cache(fn, { ttl: 30000 })
cruel.abort(fn, { signal })

composition

const wrapped = cruel.compose(fetcher, {
  retry: { attempts: 3, delay: 1000, backoff: "exponential" },
  circuitBreaker: { threshold: 5, timeout: 30000 },
  timeoutMs: 5000,
  cache: { ttl: 10000 },
  timeout: 0.05,
  corrupt: 0.02,
  fail: 0.05,
  delay: [100, 300],
})

timeoutMs applies wrapper timeout in milliseconds. timeout remains chaos probability.

cruel.wrap(fetcher).fail(0.1)
cruel.wrap(fetcher).slow([100, 500])
cruel.wrap(fetcher).timeout(0.05)

presets and scenarios

cruel.enable(cruel.presets.development)
cruel.enable(cruel.presets.nightmare)
cruel.enable(cruel.presets.apocalypse)

await cruel.play("networkPartition")
await cruel.play("highLatency")
await cruel.play("degraded")
await cruel.play("recovery")
cruel.stop()

diagnostics

const off = cruel.on((event) => {
  console.log(event.type, event.target)
})

const stats = cruel.stats()
cruel.resetStats()
off()

fetch interception

cruel.patchFetch()

cruel.intercept("api.openai.com", {
  rateLimit: { rate: 0.1, retryAfter: 60 },
  delay: [100, 500],
  slowBody: [100, 300],
  headers: { "x-chaos": "true" },
  truncate: 0.05,
  malformed: 0.02,
})

cruel.unpatchFetch()
cruel.clearIntercepts()

utilities

cruel.seed(12345)
cruel.coin(0.5)
cruel.pick([1, 2, 3])
cruel.between(10, 20)
cruel.maybe("value", 0.5)
await cruel.delay([100, 300])