Introducing Effect Kit
Our first two packages bring Redis and email into Effect, with errors that say how far a request got before it failed, so you know when to retry.
- VC
Vedant ChainaniMaintainer
4 min read

Effect Kit is a set of open-source packages that bring the infrastructure your app depends on into Effect.
This week we published the first two: @effect-kit/redis and @effect-kit/emails.
The question a Promise can't answer
If you talk to Redis or send email from Effect today, you probably wrap a Promise client in Effect.tryPromise. That works well, right up until a call fails halfway.
The connection drops while you're writing. Did Redis apply the INCR? The email provider times out. Is the customer about to get two receipts?
The wrapped Promise can't tell you. It knows the request failed. It doesn't know how far it got, and that is the one thing you need to decide what to do next.
We built Effect Kit around that question.
Every failure says how far it got
In @effect-kit/redis, a dropped connection fails with a ConnectionError, and its delivery field says where the command was when it happened.
"unsent" means nothing was written. The server never saw the command, so sending it again is safe.
"possibly-written" means the bytes may have reached the server, but no reply came back. The command may have run once, or not at all.
The client never sends a command twice on its own. Retrying is your call, and the error carries what you need to make it:
import { , } from "effect";
import { } from "@effect-kit/redis";
const = .(function* () {
const = yield* ;
return yield* .("order:42", "paid");
}).(
.({
: .("200 millis"),
: 5,
: () => . === "ConnectionError" && . === "unsent",
}),
);Here's that same situation as a machine you can break. Throw the switch before the write, then after it, and watch the error change.
The difference matters more for some commands than others. Setting order:42 to "paid" twice is harmless. Incrementing a counter twice is a bug, and nothing in a plain error message will tell you which case you're in.
Email has the same problem with a different name. Every EmailError from @effect-kit/emails carries an acceptance field:
"notSubmitted": nothing reached the provider. Send it again."rejected": the provider refused it. Sending the same message again won't help, so fix it first."unknown": the provider may have accepted it. Retry only with an idempotency key on a provider that honors one.
Some failures never need a request at all. Each provider declares what it can carry, and email.send checks the message against that before calling the provider. Send an attachment through MailerSend and you get an UnsupportedFeatureError with acceptance: "notSubmitted", before anything leaves your process.
Swap the Layer, keep the program
Each package gives you one service, Redis or Email, and a set of Layers that provide it. Your program only ever asks for the service:
import { } from "effect";
import { } from "@effect-kit/emails";
const = (: string) =>
.(function* () {
const = yield* ;
return yield* .({
: "hello@example.com",
,
: "Welcome",
: "Your account is ready.",
});
});Where the email actually goes is decided once, at the edge of the app:
.(.(.()));
.(.(.()));
.(.(.()));Moving from Resend to Postmark is a one-line change. So is a test: EmailTest.layer() runs the same validation and capability checks as a real provider, then records the message in an outbox you can assert on instead of sending it.
Redis works the same way across runtimes. RedisNode.layer and RedisBun.layer open native sockets, and RedisUpstash.layerConfig talks HTTP for places without them, like Deno and Cloudflare Workers.
Try it
Both retry examples are in the playground below, under the Effect.retry tab of each package, next to pipelines, scopes, batches and capability checks. They run against a simulated Redis and a simulated email provider, so you can pick where things fail.
Simulated Redis: every round trip takes 400ms.
const name = yield* redis.set("{user:42}:name", "Ada");
const visits = yield* redis.incr("{user:42}:visits");
const plan = yield* redis.get("{user:42}:plan");
Inspired by Kit Langton's Visual Effect
What's in the first release
Redis. A native client that speaks RESP itself, so Effect is its only dependency. Commands are typed, connections are scoped and close with your program, and it covers pipelines, transactions, Pub/Sub, Streams, Cluster and Sentinel. It runs against Redis, Valkey, Dragonfly, KeyDB, Redict, Garnet, Apache Kvrocks and Upstash. It's already on 0.1.1, which added typed batch and reusable compile plans. The quick start takes a few minutes.
Emails. One Email service across 16 providers, including Resend, Amazon SES, Postmark, SendGrid, Mailgun and SMTP. You install only the SDK for the provider you use. EmailReact.render turns a React Email component into HTML and plain-text bodies, and email.sendMany sends up to 1,000 messages with bounded concurrency, returning one result per message. Start with the quick start.
What's next
Storage is next: put, scoped get, head, delete and paginated listing, in the same shape as the other two. One service, a Layer per backend, and errors you can act on.
Effect Kit is Apache-2.0 and developed in the open on GitHub. If something is missing or broken, open an issue. Updates go out on X at @effectkit and here on the blog.
Thanks for reading. Now go break a connection on purpose.