Typed, Effect-native TypeScript SDKs for 85 cloud providers, generated straight from each one's own API description. No hand-written client to fall behind the API — and when the description is wrong, the fix is a patch, not a fork.
Every operation returns an Effect. So the things an SDK usually bolts on — error handling, retries, pagination, configuration, tracing — are the Effect features you already use, with the provider's specifics filled in.
Match the exact error with catchTags, or a category — throttling, not-found, conflict — with catchIf. Nothing is unknown, and the compiler tells you when you've missed one.
Retries and backoff
// Default: exponential from 250ms, cap 5s, jitter,// 8 tries, transient/throttling only, Retry-After honoured.
program.pipe(AWS.Retry.throttling) // retry throttles forever
program.pipe(AWS.Retry.none) // or not at all
program.pipe(AWS.Retry.policy({ // or your own
while: isTransientError,
schedule: Schedule.exponential("100 millis")
.pipe(Schedule.recurs(3)),
}))
Transient and throttling errors are retried with backoff and jitter; a 404 never is. The API's own Retry-After is respected. Override the policy for one call or for the whole program — it's a Layer.
Streaming pagination
// Paginated: pages fetch themselves, on demand.const stale = Lambda.listFunctions.items({}).pipe(
Stream.filter((fn) => fn.Runtime === "nodejs16.x"),
Stream.take(50),
Stream.runCollect,
)
// Bodies stream in and out — nothing is buffered.const copy = S3.getObject({ Bucket: src, Key }).pipe(
Effect.flatMap((o) =>
S3.putObject({ Bucket: dst, Key, Body: o.Body! })),
)
Every paginated operation has .items() and .pages(). Large bodies — an S3 object, an upload — stream in and out without being held in memory. Cancel the Effect and the requests stop.
Your code calls S3.getObject; where the credentials come from is decided at the edge of the program. In tests, swap the HTTP client for a fake and nothing else changes — that is how the benchmarks on this site run without a network.
OpenTelemetry spans
// Effect's HttpClient opens a span per request with the// standard http.* and server.* attributes.
program.pipe(
Effect.withSpan("provision-bucket"),
Effect.provide(OtlpTracer.layer({
url: "http://collector:4318/v1/traces",
})),
)
They are ordinary Effect spans, so they nest inside yours and go wherever your OpenTelemetry exporter sends them — Axiom, Datadog, Honeycomb, a local collector.
Per-operation imports
// the bundle keeps getObject — not the other 111 S3 opsimport { getObject } from"@distilled.cloud/aws/s3"
gzipped, one S3 operation
118.4 KB
gzipped, one Workers operation
61.7 KB
Cloudflare call, p50
31 µs
AWS call incl. SigV4, p50
442 µs
Every operation is its own export, so the bundler keeps only the ones you call — 1 of 112 S3 operations survives in the measured build. Runs on Node, Bun and Workers. See all the numbers →
The problem
The spec should be the only source. It never is.
Every generated SDK — ours, the vendor's, the one you'd write — is only as true as the API description it was built from. Most of those descriptions are written by hand, beside the API rather than from it. So they drift: a field marked required that the server omits, a value documented as a string that arrives null, a property the response always carries that the spec never mentions — and, almost universally, silence about what happens when a call fails. The types compile. The happy path works. Then a real response doesn't match, and your typed client either rejects a valid payload or hands you an untyped error.
What Fly's spec declaresPOST /v1/apps/{app}/machines
responses:"200": OK
— that's it —
One response. No 400 for a bad config, no 403 for a scoped token, no 409 when the name is taken.
Alchemy calls it for real
What Distilled shipsFly.Machines.createMachine
export typeCreateMachineError =
| BadRequest// 400 · seen live | Forbidden// 403 · seen live | NotFound// 404 | Conflict// 409 · name-taken race
| FlyIoOpError
Four typed failures in the operation's error union, each added as a patch to the spec the moment a real deploy surfaced it.
25,345spec fixes, and counting
20 of 85providers have needed at least one — including 10 of the 12 that Alchemy runs on
1 in 4operations needed a fix on the median provider Alchemy runs on The full tally →
You could patch the generated code by hand — and lose it on the next regeneration. Or you could fix the description once, and every build after it inherits the correction.
How it works
Generated from the spec. Proven by Alchemy.
Distilled turns each provider's API description into an SDK. Alchemy builds real infrastructure on those SDKs — resources, providers, tests against the live API. Every place the description turns out to be wrong comes back as a patch, and the next generation starts from the corrected description.
Alchemy
Infrastructure as code.
Resources: Bucket, Table, Machine
Tests against the actual API
Distilled
Effect-native SDKs.
Schemas, operations, typed errors
Retry, pagination, streaming
Spec
The provider's own API description.
Providers
85 providers, one shape.
Each provider is one package, generated from that provider's own API description and published to npm as @distilled.cloud/<provider>. Pair it with effect. Some descriptions needed more fixing than others — see the Wall of Shame →