# Retries

By default, in *addition* to the **initial attempt**, Inngest will retry a function or a step up to 4 times until it succeeds. This means that for a function with a default configuration, it will be attempted 5 times in total.

For the function below, if the database write fails then it'll be retried up to 4 times until it succeeds:

```ts
inngest.createFunction(
  { id: "click-recorder", triggers: { event: "app/button.clicked" } },
  async ({ event, attempt }) => {
    await db.clicks.insertOne(event.data); // this code now retries!
  },
);
```

```go
inngestgo.CreateFunction(
	client,
	inngestgo.FunctionOpts{ID: "click-recorder"},
	inngestgo.EventTrigger("app/button.clicked", nil),
	func(ctx context.Context, input inngestgo.Input[ButtonClickedEvent]) (any, error) {
		result, err := db.Clicks.InsertOne(input.Event.Data)
		return result, err
	},
)
```

```python
@inngest_client.create_function(
    fn_id="click-recorder",
    trigger=inngest.TriggerEvent(event="app/button.clicked"),
)
def record_click(ctx: inngest.Context) -> None:
    db.clicks.insert_one(ctx.event.data)
```

You can configure the number of `retries` by specifying it in your function configuration. Setting the value to `0` will disable retries.

```ts
inngest.createFunction(
  {
    id: "click-recorder",
    triggers: { event: "app/button.clicked" },
    retries: 10, // choose how many retries you'd like
  },
  async ({ event, step, attempt }) => { /* ... */ },
);
```

```go
inngestgo.CreateFunction(
	client,
	inngestgo.FunctionOpts{
		ID:      "click-recorder",
		Retries: 10, // choose how many retries you'd like
	},
	inngestgo.EventTrigger("app/button.clicked", nil),
	func(ctx context.Context, input inngestgo.Input[ButtonClickedEvent]) (any, error) {
		// ...
		return nil, nil
	},
)
```

```python
@inngest_client.create_function(
    fn_id="click-recorder",
    retries=10,  # choose how many retries you'd like
    trigger=inngest.TriggerEvent(event="app/button.clicked"),
)
def click_recorder(ctx: inngest.Context) -> None:
    # ...
```

You can customize the behavior of your function based on the number of retries using the `attempt` argument. `attempt` is passed in the function handler's context and is zero-indexed, meaning the first attempt is `0`, the second is `1`, and so on. The `attempt` is incremented every time the function throws an error and is retried, and is reset when steps complete. This allows you to handle attempt numbers differently in each step.

Retries will be performed with backoff according to [the default schedule](https://github.com/inngest/inngest/blob/main/pkg/backoff/backoff.go#L10-L22).

## Steps and Retries

A function can be broken down into multiple steps, where each step is individually executed and retried.

**Each `step.run()` has its own independent retry counter.** The retry configuration you set on your function applies to each individual step, not as a shared pool across all steps. For example, if you configure a function with 5 retries, each `step.run()` will be retried up to 5 times independently.

In the example below, both the "*get-data*" and "*save-data*" steps each get their own set of 4 retries (5 total attempts including the initial attempt). If the "*save-data*" step fails all 5 attempts, it doesn't affect the retry count of the "*get-data*" step - they are completely independent.

```ts
inngest.createFunction(
  { id: "sync-systems", triggers: { event: "auto/sync.request" } },
  async ({ step }) => {
    // Can be retried up to 4 times
    const data = await step.run("get-data", async () => {
      return getDataFromExternalSource();
    });

    // Can also be retried up to 4 times
    await step.run("save-data", async () => {
      return db.syncs.insertOne(data);
    });
  },
);
```

```go
inngestgo.CreateFunction(
	client,
	inngestgo.FunctionOpts{ID: "sync-systems"},
	inngestgo.EventTrigger("auto/sync.request", nil),
	func(ctx context.Context, input inngestgo.Input[SyncRequestEvent]) (any, error) {
		// can be retried up to 4 times
		data, err := step.Run(ctx, "get-data", func(ctx context.Context) (any, error) {
			return getDataFromExternalSource()
		})
		if err != nil {
			return nil, err
		}

		// can also be retried up to 4 times
		_, err = step.Run(ctx, "save-data", func(ctx context.Context) (any, error) {
			return db.Syncs.InsertOne(data.(DataType))
		})
		if err != nil {
			return nil, err
		}

		return nil, nil
	},
)
```

```python
@inngest_client.create_function(
    fn_id="sync-systems",
    trigger=inngest.TriggerEvent(event="auto/sync.request"),
)
def sync_systems(ctx: inngest.ContextSync) -> None:
    # Can be retried up to 4 times
    data = ctx.step.run("Get data", get_data_from_external_source)

    # Can also be retried up to 4 times
    ctx.step.run("Save data", db.syncs.insert_one, data)
```

> **Callout:** Important: Each step gets its own independent retry counter. You can configure the number of retries for each function. This excludes the initial attempt. A retry count of 4 means that each individual step will be attempted up to 5 times (1 initial attempt + 4 retries).For example, if you have a function with 3 steps and retries: 4 configured, each of the 3 steps can be retried up to 4 times independently. This means you could theoretically have up to 15 total attempts across all steps (5 attempts × 3 steps) if every step fails every time.

## Preventing retries with Non-retriable errors

You can throw a [non-retriable error](/docs-markdown/reference/typescript/functions/errors#non-retriable-error) from a step or a function, which will bypass any remaining retries and fail the step or function it was thrown from.

This is useful for when you know an error is permanent and want to stop all execution. In this example, the user doesn't exist, so there's no need to continue to email them.

```ts
import { NonRetriableError } from "inngest";

inngest.createFunction(
  { id: "user-weekly-digest", triggers: { event: "user/weekly.digest.requested" } },
  async ({ event, step }) => {
    const user = await step
      .run("get-user-email", () => {
        return db.users.findOne(event.data.userId);
      })
      .catch((err) => {
        if (err.name === "UserNotFoundError") {
          throw new NonRetriableError("User no longer exists; stopping");
        }

        throw err;
      });

    await step.run("send-digest", () => {
      return sendDigest(user.email);
    });
  },
);
```

```go
inngestgo.CreateFunction(
	client,
	inngestgo.FunctionOpts{ID: "user-weekly-digest"},
	inngestgo.EventTrigger("user/weekly.digest.requested", nil),
	func(ctx context.Context, input inngestgo.Input[WeeklyDigestRequestedEvent]) (any, error) {
		user, err := step.Run(ctx, "get-user-email", func(ctx context.Context) (any, error) {
			return db.Users.FindOne(input.Event.Data.UserID)
		})
		if err != nil {
			if stepErr, ok := err.(step.StepError); ok && stepErr.Name == "UserNotFoundError" {
				return nil, inngestgo.NoRetryError(fmt.Errorf("User no longer exists; stopping"))
			}
			return nil, err
		}

		_, err = step.Run(ctx, "send-digest", func(ctx context.Context) (any, error) {
			return sendDigest(user.(UserType).Email)
		})
		if err != nil {
			return nil, err
		}

		return nil, nil
	},
)
```

```python
from inngest.errors import NonRetriableError

@inngest_client.create_function(
    fn_id="user-weekly-digest",
    trigger=inngest.TriggerEvent(event="user/weekly.digest.requested"),
)
def user_weekly_digest(ctx: inngest.ContextSync) -> None:
    try:
        user = ctx.step.run("get-user-email", db.users.find_one, ctx.event.data["userId"])
    except Exception as err:
        if err.name == "UserNotFoundError":
            raise NonRetriableError("User no longer exists; stopping")
        raise

    ctx.step.run("send-digest", send_digest, user["email"])
```

## Customizing retry times

Retries are executed with exponential back-off with some jitter, but it's also possible to specify exactly when you'd like a step or function to be retried.

In this example, an external API provided `Retry-After` header with information on when requests can be made again, so you can tell Inngest to retry your function then.

```ts
import { RetryAfterError } from 'inngest';

inngest.createFunction(
  { id: "send-welcome-notification", triggers: { event: "app/user.created" } },
  async ({ event, step }) => {

    const msg = await step.run('send-message', async () => {
      const { success, retryAfter, message } = await twilio.messages.create({
        to: event.data.user.phoneNumber,
        body: "Welcome to our service!",
      });

      if (!success && retryAfter) {
        throw new RetryAfterError("Hit Twilio rate limit", retryAfter);
      }
      
      return { message };
    });
    
  },
);
```

```go
inngestgo.CreateFunction(
	client,
	inngestgo.FunctionOpts{ID: "send-welcome-notification"},
	inngestgo.EventTrigger("user.created", nil),
	func(ctx context.Context, input inngestgo.Input[SignedUpEvent]) (any, error) {
		success, retryAfter, err := twilio.Messages.Create(twilio.MessageOpts{
			To:   input.Event.Data.User.PhoneNumber,
			Body: "Welcome to our service!",
		})
		if err != nil {
			return nil, err
		}

		if !success && retryAfter != nil {
			return nil, inngestgo.RetryAtError(fmt.Errorf("Hit Twilio rate limit"), *retryAfter)
		}

		return nil, nil
	}
)
```

```python
import inngest
from src.inngest.client import inngest_client

@inngest_client.create_function(
    fn_id="send-welcome-notification",
    trigger=inngest.TriggerEvent(event="user.created"),
)
def send_welcome_notification(ctx: inngest.ContextSync) -> None:
	success, retryAfter, err = twilio.Messages.Create(twilio.MessageOpts{
		To:   ctx.event.data["user"]["phoneNumber"],
		Body: "Welcome to our service!",
	})

	if not success and retryAfter is not None:
		raise inngest.RetryAfterError("Hit Twilio rate limit", retryAfter)
```