Dio Retry It ๐Ÿš€

A Smarter retry interceptor for Dio with exponential backoff and full jitter based on dio_smart_retry

Pub Version Dart SDK Version style: very good analysis License

โœจ Why Dio Retry It?

When building production apps, network failures are inevitable. Dio Retry It automatically handles these failures with an intelligent retry strategy that protects your servers from traffic spikes and improves user experience.

๐ŸŽฏ Key Features

  • โšก Smart Retry Logic - Automatically retries failed requests based on configurable rules
  • ๐Ÿ“ˆ Exponential Backoff with Full Jitter - Prevents thundering herd problems when servers recover
  • ๐ŸŽฒ Random Delays - Avoids synchronized retry storms from thousands of clients
  • ๐Ÿ”„ FormData Support - Automatically clones FormData before retrying
  • ๐Ÿšซ Per-Request Control - Disable retries for specific requests when needed
  • ๐Ÿ“ Built-in Logging - Track retry attempts with custom log functions
  • ๐Ÿ”’ Null Safety - Fully migrated to sound null safety
  • โš™๏ธ Highly Configurable - Customize every aspect of the retry behavior

๐Ÿ“‘ Table of Contents

๐Ÿš€ Getting Started

Installation

Add to your pubspec.yaml:

dependencies:
  dio_retry_it: ^8.0.0

Then import it:

import 'package:dio_retry_it/dio_retry_it.dart';

Basic Usage

final dio = Dio();

// Add the retry interceptor
dio.interceptors.add(
  RetryInterceptor(
    dio: dio,
    maxAttempts: 3,              // Retry up to 3 times
    baseDelay: Duration(milliseconds: 500),  // Start with 500ms
    maxDelay: Duration(seconds: 10),         // Cap at 10 seconds
    backoffFactor: 2.0,          // Double the delay each attempt
    logPrint: print,             // Optional: log retry attempts
  ),
);

// Now every failed request will be automatically retried
try {
  await dio.get('https://api.example.com/data');
} catch (e) {
  // After all retries fail, the error will be thrown
  print('Request failed after retries: $e');
}

๐ŸŽฏ How It Works

The Retry Strategy

Unlike simple fixed-delay retries, Dio Retry It uses exponential backoff with full jitter - the gold standard for distributed systems.

The Algorithm:

  1. Calculate exponential delay: baseDelay ร— backoffFactor^(attempt-1)
  2. Cap at maxDelay
  3. Apply full jitter: pick a random delay between 0 and the capped value

Example with defaults (baseDelay: 500ms, backoffFactor: 2.0, maxDelay: 10s):

Attempt Calculation Delay Range
1 500ms ร— 2โฐ 0-500ms
2 500ms ร— 2ยน 0-1,000ms
3 500ms ร— 2ยฒ 0-2,000ms
4 500ms ร— 2ยณ 0-4,000ms
5 500ms ร— 2โด 0-8,000ms
6+ Capped at 10s 0-10,000ms

๐Ÿ›ก๏ธ Why Full Jitter Matters

Imagine thousands of clients all hitting a server that's temporarily down. When the server recovers, without jitter, all clients would retry at the exact same moment, causing a massive traffic spike - effectively a DDoS attack on your own servers!

Full jitter solves this by randomizing retry times, spreading the load evenly across the recovery window.

Key Insight: Random jitter matters far more for real-world reliability than a fixed delay schedule. This is why major cloud providers like AWS and Google Cloud recommend jitter-based retries.

๐Ÿ“– Advanced Usage

Custom Retry Logic

Sometimes you need finer control over what gets retried:

final dio = Dio();

dio.interceptors.add(
  RetryInterceptor(
    dio: dio,
    maxAttempts: 5,
    retryEvaluator: (error, attempt) async {
      // Only retry specific errors
      if (error.type == DioExceptionType.connectionTimeout) return true;
      if (error.type == DioExceptionType.receiveTimeout) return true;
      
      // Retry server errors but not client errors
      if (error.type == DioExceptionType.badResponse) {
        final status = error.response?.statusCode ?? 0;
        return status >= 500 && status < 600;
      }
      
      return false;
    },
  ),
);

Custom Retry Delays

Configure retry behavior for your specific needs:

dio.interceptors.add(
  RetryInterceptor(
    dio: dio,
    maxAttempts: 5,
    baseDelay: Duration(seconds: 1),     // Start with 1 second
    maxDelay: Duration(minutes: 1),      // Cap at 1 minute
    backoffFactor: 1.5,                  // Slower growth
    logPrint: (msg) => debugPrint(msg),
  ),
);

Disable Retry for Specific Requests

Some requests shouldn't be retried (e.g., idempotent operations):

// Using RequestOptions
final request = RequestOptions(path: '/user/delete/123')
  ..disableRetry = true;
await dio.fetch(request);

// Using Options
final response = await dio.get(
  'https://api.example.com/status',
  options: Options(extra: {'disableRetry': true}),
);

Working with FormData

Dio Retry It automatically clones FormData before retrying - no extra work needed!

final formData = FormData.fromMap({
  'file': await MultipartFile.fromFile('image.jpg'),
  'name': 'My Image',
});

// This will retry automatically if it fails
final response = await dio.post(
  'https://api.example.com/upload',
  data: formData,
);

โš™๏ธ Configuration Reference

Constructor Parameters

Parameter Type Default Description
dio Dio Required The Dio instance used for retries
maxAttempts int 3 Maximum retry attempts (excludes original)
baseDelay Duration 500ms Starting delay before backoff
maxDelay Duration 10s Maximum delay cap
backoffFactor double 2.0 Multiplier per attempt (must be โ‰ฅ1)
retryEvaluator RetryEvaluator? Default evaluator Custom retry decision logic
logPrint Function(String)? null Logging callback

Default Retry Status Codes

By default, responses with these status codes are retried:

  • 408 - Request Timeout
  • 429 - Too Many Requests
  • 500 - Internal Server Error
  • 502 - Bad Gateway
  • 503 - Service Unavailable
  • 504 - Gateway Timeout
  • 440 - Login Timeout (IIS)
  • 460 - Client Closed Request (AWS ELB)
  • 499 - Client Closed Request (nginx)
  • 520 - Web Server Unknown Error
  • 521 - Web Server Is Down
  • 522 - Connection Timed Out
  • 523 - Origin Unreachable
  • 524 - Timeout Occurred
  • 525 - SSL Handshake Failed
  • 527 - Railgun Error
  • 598 - Network Read Timeout
  • 599 - Network Connect Timeout

Extending Status Codes

// Add your own status codes
final evaluator = DefaultRetryEvaluator(
  {...defaultRetryableStatuses, 401, 403},
);

dio.interceptors.add(
  RetryInterceptor(
    dio: dio,
    retryEvaluator: evaluator.evaluate,
  ),
);

๐Ÿ”„ Migration Guide

From dio_smart_retry to dio_retry_it

Key Changes

dio_smart_retry dio_retry_it
retries: 3 maxAttempts: 3
retryDelays: [...] baseDelay + backoffFactor + maxDelay
retryableExtraStatuses Custom retryEvaluator
Fixed delays Exponential backoff with full jitter โœ…
Manual FormData handling Automatic cloning โœ…

Migration Examples

Basic Retry

Before:

RetryInterceptor(
  dio: dio,
  retries: 3,
  retryDelays: const [
    Duration(seconds: 1),
    Duration(seconds: 3),
    Duration(seconds: 5),
  ],
)

After:

RetryInterceptor(
  dio: dio,
  maxAttempts: 3,
  baseDelay: Duration(seconds: 1),
  maxDelay: Duration(seconds: 10),
  backoffFactor: 2.0,
)

Custom Status Codes

Before:

RetryInterceptor(
  dio: dio,
  retries: 3,
  retryableExtraStatuses: {401, 403},
)

After:

RetryInterceptor(
  dio: dio,
  maxAttempts: 3,
  retryEvaluator: (error, attempt) {
    if (error.type == DioExceptionType.badResponse) {
      final status = error.response?.statusCode;
      return status == 401 || status == 403;
    }
    return false;
  },
)

FormData

Before:

// Manual handling was needed
final formData = FormData.fromMap({'file': file});
// Interceptor called _recreateOptions internally

After:

// Automatic cloning - works out of the box!
final formData = FormData.fromMap({'file': file});
// No extra code needed

Removed Parameters

Parameter Replacement
retryableExtraStatuses Custom retryEvaluator
ignoreRetryEvaluatorExceptions Built-in error handling
retryDelays baseDelay + backoffFactor

โœ… Migration Checklist

  • Replace retries โ†’ maxAttempts
  • Replace retryDelays โ†’ baseDelay + backoffFactor
  • Add maxDelay parameter
  • Update custom status codes to use retryEvaluator
  • Remove ignoreRetryEvaluatorExceptions if used
  • Test your app

Benefit: New package prevents thundering herd problems with exponential backoff + full jitter - critical for production apps with many concurrent users! ๐Ÿš€

๐Ÿ“Š Performance & Best Practices

๐ŸŽฏ When to Use Retries

โœ… Good Candidates:

  • GET requests (idempotent)
  • Network timeouts
  • Temporary server errors (5xx)
  • Rate limiting (429)
  • Connection issues

โŒ Avoid Retries For:

  • POST/PUT/DELETE mutations (unless idempotent)
  • Client errors (4xx except 429)
  • Cancelled requests
  • Format/serialization errors

๐Ÿ“ˆ Production Recommendations

// Recommended production configuration
dio.interceptors.add(
  RetryInterceptor(
    dio: dio,
    maxAttempts: 3,
    baseDelay: Duration(milliseconds: 1000),
    maxDelay: Duration(seconds: 30),
    backoffFactor: 2.0,
    logPrint: (msg) {
      // Only log in development
      if (kDebugMode) print(msg);
    },
  ),
);

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

๐Ÿ“„ License

Distributed under the MIT License. See LICENSE for more information.

๐Ÿ™ Acknowledgments

This package is a next-generation fork of the abandoned dio_smart_retry package, rebuilt with:

  • โœ… Modern Dart practices
  • โœ… Full null safety
  • โœ… Exponential backoff with full jitter
  • โœ… Better FormData handling
  • โœ… Improved error handling
  • โœ… Better test coverage

๐Ÿ“ž Support


Made with โค๏ธ for the Dart community


ยฉ 2026 Silify

Libraries

dio_retry_it
Retry library for Dio package made with love