# Introducing Effect Kit

> For the complete documentation index, see [llms.txt](https://effectkit.dev/llms.txt).

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.

Published October 30, 2026. Category: Announcement. Tags: redis, emails, effect.

Source: https://effectkit.dev/blog/introducing-effect-kit

Effect Kit is a set of open-source packages that bring the infrastructure your app depends on into [Effect](https://effect.website).

This week we published the first two: `@effect-kit/redis` and `@effect-kit/emails`.

## The question a Promise can't answer [#the-question-a-promise-cant-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 [#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:

```ts twoslash
import { Effect, Schedule } from "effect";
import { Redis } from "@effect-kit/redis";

const markPaid = Effect.gen(function* () {
  const redis = yield* Redis;
  return yield* redis.set("order:42", "paid");
}).pipe(
  Effect.retry({
    schedule: Schedule.exponential("200 millis"),
    times: 5,
    while: (error) => error._tag === "ConnectionError" && error.delivery === "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.

<RedisWrite
  fig={1}
  caption="Cut the connection before or after the write. Only one of them is safe to retry."
/>

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 [#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:

```ts twoslash
import { Effect } from "effect";
import { Email } from "@effect-kit/emails";

const welcome = (to: string) =>
  Effect.gen(function* () {
    const email = yield* Email;
    return yield* email.send({
      from: "hello@example.com",
      to,
      subject: "Welcome",
      text: "Your account is ready.",
    });
  });
```

Where the email actually goes is decided once, at the edge of the app:

```ts twoslash
import { Effect } from "effect";
import { Email } from "@effect-kit/emails";
import * as Resend from "@effect-kit/emails/resend";
import * as Postmark from "@effect-kit/emails/postmark";
import { EmailTest } from "@effect-kit/emails/testing";

const welcome = Effect.gen(function* () {
  const email = yield* Email;
  return yield* email.send({
    from: "hello@example.com",
    to: "ada@example.com",
    subject: "Welcome",
    text: "Your account is ready.",
  });
});

// ---cut---
welcome.pipe(Effect.provide(Resend.layerConfig()));
welcome.pipe(Effect.provide(Postmark.layerConfig()));
welcome.pipe(Effect.provide(EmailTest.layer()));
```

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.

<SwapLayer
  fig={2}
  caption="Turn a knob to swap a Layer. The program in the middle never changes."
/>

## Try it [#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.

<Playground caption="Everything runs in your browser. Nothing here talks to a real server." />

## What's in the first release [#whats-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](/docs/redis) 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](/docs/emails).

## What's next [#whats-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](https://github.com/effect-kit). If something is missing or broken, open an issue. Updates go out on X at [@effectkit](https://x.com/effectkit) and here on the blog.

Thanks for reading. Now go break a connection on purpose.
