Skip to main content

Timeout Strategy

The Timeout Strategy prevents operations from running indefinitely by cancelling them after a specified duration. This is crucial for maintaining responsive applications and preventing resource exhaustion.

When to Use Timeout

Timeouts are essential for:

  • 🌐 Network operations that might hang indefinitely
  • 🗄️ Database queries that could lock resources
  • 📁 File operations that might block on I/O
  • 🔄 External service calls with unpredictable response times
  • 📱 User-facing operations that need to stay responsive
Timeout vs Cancellation

Polly Dart's timeout strategy integrates with Dart's cancellation tokens, providing clean cancellation semantics rather than forceful termination.

Basic Usage

Simple Timeout

import 'package:polly_dart/polly_dart.dart';

final pipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(seconds: 10)) // 10-second timeout
.build();

final result = await pipeline.execute((context) async {
return await longRunningOperation();
});

Configured Timeout

final pipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions(
timeout: Duration(seconds: 30),
onTimeout: (args) async {
logger.warning('Operation timed out after ${args.timeout}');
},
))
.build();

Configuration Options

TimeoutStrategyOptions

PropertyTypeDefaultDescription
timeoutDurationRequiredFixed timeout duration
timeoutGeneratorTimeoutGenerator?nullDynamic timeout calculation
onTimeoutOnTimeoutCallback?nullCallback invoked when timeout occurs

Dynamic Timeouts

Context-Based Timeouts

Calculate timeouts based on operation context:

final dynamicTimeoutPipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions.withGenerator(
timeoutGenerator: (args) async {
final context = args.context;

// Different timeouts for different operations
final operationKey = context.operationKey ?? '';

if (operationKey.contains('critical')) {
return Duration(seconds: 5); // Fast timeout for critical ops
} else if (operationKey.contains('bulk')) {
return Duration(minutes: 5); // Longer timeout for bulk ops
} else if (operationKey.contains('report')) {
return Duration(minutes: 10); // Even longer for reports
}

// Default timeout
return Duration(seconds: 30);
},
))
.build();

Adaptive Timeouts

Adjust timeouts based on historical performance:

class AdaptiveTimeoutManager {
final Map<String, Duration> _averageResponseTimes = {};
final Map<String, List<Duration>> _recentResponses = {};

Duration calculateTimeout(String operationKey) {
final recentTimes = _recentResponses[operationKey] ?? [];

if (recentTimes.isEmpty) {
return Duration(seconds: 30); // Default
}

// Calculate average response time
final totalMs = recentTimes.fold<int>(
0,
(sum, duration) => sum + duration.inMilliseconds,
);
final averageMs = totalMs / recentTimes.length;

// Set timeout to 3x average response time, with bounds
final timeoutMs = (averageMs * 3).clamp(1000, 120000); // 1s to 2min
return Duration(milliseconds: timeoutMs.toInt());
}

void recordResponseTime(String operationKey, Duration responseTime) {
_recentResponses.putIfAbsent(operationKey, () => <Duration>[]);
final recent = _recentResponses[operationKey]!;

recent.add(responseTime);

// Keep only last 10 measurements
if (recent.length > 10) {
recent.removeAt(0);
}
}
}

final adaptiveManager = AdaptiveTimeoutManager();

final adaptivePipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions.withGenerator(
timeoutGenerator: (args) async {
final operationKey = args.context.operationKey ?? 'default';
return adaptiveManager.calculateTimeout(operationKey);
},
))
.build();

Time-of-Day Based Timeouts

Adjust timeouts based on expected system load:

final timeBasedPipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions.withGenerator(
timeoutGenerator: (args) async {
final now = DateTime.now();
final hour = now.hour;

// Longer timeouts during peak hours (9-17)
if (hour >= 9 && hour < 17) {
return Duration(minutes: 2); // Peak hours
} else if (hour >= 22 || hour < 6) {
return Duration(seconds: 15); // Maintenance window
} else {
return Duration(seconds: 45); // Off-peak hours
}
},
))
.build();

Timeout Monitoring

Timeout Callbacks

Track timeout patterns for optimization:

final monitoredPipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions(
timeout: Duration(seconds: 30),
onTimeout: (args) async {
final operationKey = args.context.operationKey ?? 'unknown';
final timeoutDuration = args.timeout;

// Log timeout events
logger.warning(
'Operation $operationKey timed out after $timeoutDuration'
);

// Emit metrics
metrics.incrementCounter('operation_timeouts', tags: {
'operation': operationKey,
'timeout_seconds': timeoutDuration.inSeconds.toString(),
});

// Record for adaptive timeout calculation
timeoutTracker.recordTimeout(operationKey, timeoutDuration);

// Alert on frequent timeouts
final recentTimeouts = timeoutTracker.getRecentTimeouts(operationKey);
if (recentTimeouts.length > 5) {
await alertService.send(
'Frequent Timeouts',
'Operation $operationKey has timed out ${recentTimeouts.length} times recently',
);
}
},
))
.build();

Exception Handling

Timeout Exceptions

try {
final result = await timeoutPipeline.execute((context) async {
return await slowOperation();
});
} on TimeoutRejectedException catch (e) {
// Handle timeout specifically
logger.warning('Operation timed out: ${e.message}');
return getDefaultValue();
} catch (e) {
// Handle other exceptions
logger.error('Operation failed: $e');
rethrow;
}

Graceful Timeout Handling

Future<ApiResponse> fetchDataWithGracefulTimeout() async {
try {
return await timeoutPipeline.execute((context) async {
return await apiClient.fetchData();
});
} on TimeoutRejectedException {
// Return partial data or cached data on timeout
return ApiResponse.partial(
data: await getCachedData(),
message: 'Request timed out, showing cached data',
);
}
}

Real-World Examples

HTTP Client with Smart Timeouts

class TimeoutAwareHttpClient {
final HttpClient _client = HttpClient();
late final ResiliencePipeline _pipeline;
final Map<String, Duration> _endpointTimeouts = {};

TimeoutAwareHttpClient() {
_pipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions.withGenerator(
timeoutGenerator: _calculateTimeout,
onTimeout: _onTimeout,
))
.build();
}

Future<HttpClientResponse> get(String url) async {
final uri = Uri.parse(url);
final context = ResilienceContext(operationKey: 'GET:${uri.path}');

return await _pipeline.execute((ctx) async {
final request = await _client.getUrl(uri);
return await request.close();
}, context: context);
}

Future<Duration> _calculateTimeout(TimeoutGeneratorArguments args) async {
final operationKey = args.context.operationKey ?? '';

// Check for endpoint-specific timeout configuration
final endpointTimeout = _endpointTimeouts[operationKey];
if (endpointTimeout != null) {
return endpointTimeout;
}

// Different timeouts for different endpoint types
if (operationKey.contains('/auth/')) {
return Duration(seconds: 5); // Auth should be fast
} else if (operationKey.contains('/reports/')) {
return Duration(minutes: 2); // Reports can be slow
} else if (operationKey.contains('/realtime/')) {
return Duration(seconds: 3); // Real-time needs to be fast
} else if (operationKey.contains('/batch/')) {
return Duration(minutes: 5); // Batch operations are slower
}

// Default timeout
return Duration(seconds: 30);
}

Future<void> _onTimeout(OnTimeoutArguments args) async {
final endpoint = args.context.operationKey ?? 'unknown';

logger.warning('HTTP request timeout: $endpoint (${args.timeout})');

// Adjust timeout for this endpoint if it's timing out frequently
await _adjustEndpointTimeout(endpoint, args.timeout);
}

Future<void> _adjustEndpointTimeout(String endpoint, Duration currentTimeout) async {
// Increase timeout by 50% for this endpoint
final newTimeout = Duration(
milliseconds: (currentTimeout.inMilliseconds * 1.5).toInt(),
);

// Cap at 5 minutes
final cappedTimeout = Duration(
milliseconds: math.min(newTimeout.inMilliseconds, 300000),
);

_endpointTimeouts[endpoint] = cappedTimeout;

logger.info('Adjusted timeout for $endpoint to $cappedTimeout');
}

void dispose() {
_client.close();
}
}

Database Query with Timeout

class TimeoutAwareDatabase {
final Database _db;
late final ResiliencePipeline _queryPipeline;
late final ResiliencePipeline _transactionPipeline;

TimeoutAwareDatabase(this._db) {
// Different timeouts for different operation types
_queryPipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(seconds: 15)) // Quick queries
.build();

_transactionPipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(minutes: 2)) // Longer for transactions
.build();
}

Future<List<Map<String, dynamic>>> query(String sql, [List<dynamic>? params]) async {
return await _queryPipeline.execute((context) async {
return await _db.rawQuery(sql, params);
});
}

Future<T> transaction<T>(Future<T> Function(DatabaseTransaction) action) async {
return await _transactionPipeline.execute((context) async {
return await _db.transaction(action);
});
}

Future<User?> findUserById(int id) async {
try {
final results = await query(
'SELECT * FROM users WHERE id = ? LIMIT 1',
[id],
);

return results.isNotEmpty ? User.fromMap(results.first) : null;
} on TimeoutRejectedException {
logger.warning('Database query timed out for user $id');
return null; // Return null instead of throwing
}
}

Future<void> bulkInsert(String table, List<Map<String, dynamic>> rows) async {
// Use transaction pipeline for bulk operations
await _transactionPipeline.execute((context) async {
await _db.transaction((txn) async {
for (final row in rows) {
await txn.insert(table, row);
}
});
});
}
}

File Operations with Progressive Timeouts

class TimeoutAwareFileManager {
late final ResiliencePipeline _readPipeline;
late final ResiliencePipeline _writePipeline;

TimeoutAwareFileManager() {
_readPipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions.withGenerator(
timeoutGenerator: _calculateReadTimeout,
))
.build();

_writePipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions.withGenerator(
timeoutGenerator: _calculateWriteTimeout,
))
.build();
}

Future<String> readFile(String path) async {
final context = ResilienceContext();
context.setProperty('filePath', path);

return await _readPipeline.execute((ctx) async {
return await File(path).readAsString();
}, context: context);
}

Future<void> writeFile(String path, String content) async {
final context = ResilienceContext();
context.setProperty('filePath', path);
context.setProperty('contentSize', content.length);

await _writePipeline.execute((ctx) async {
await File(path).writeAsString(content);
}, context: context);
}

Future<Duration> _calculateReadTimeout(TimeoutGeneratorArguments args) async {
final filePath = args.context.getProperty<String>('filePath') ?? '';

try {
final file = File(filePath);
final size = await file.length();

// Base timeout of 1 second + 1 second per MB
final timeoutSeconds = 1 + (size / (1024 * 1024)).ceil();
return Duration(seconds: timeoutSeconds);
} catch (e) {
// If we can't determine size, use default
return Duration(seconds: 10);
}
}

Future<Duration> _calculateWriteTimeout(TimeoutGeneratorArguments args) async {
final contentSize = args.context.getProperty<int>('contentSize') ?? 0;

// Base timeout of 2 seconds + 2 seconds per MB
final timeoutSeconds = 2 + ((contentSize / (1024 * 1024)) * 2).ceil();
return Duration(seconds: timeoutSeconds);
}
}

Testing Timeout Behavior

Unit Testing Timeouts

import 'package:test/test.dart';
import 'package:polly_dart/polly_dart.dart';

void main() {
group('Timeout Strategy Tests', () {
test('should timeout after specified duration', () async {
final pipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(milliseconds: 100))
.build();

final stopwatch = Stopwatch()..start();

expect(
() => pipeline.execute((context) async {
await Future.delayed(Duration(milliseconds: 200));
return 'should not complete';
}),
throwsA(isA<TimeoutRejectedException>()),
);

stopwatch.stop();
expect(stopwatch.elapsedMilliseconds, lessThan(150));
});

test('should call onTimeout callback', () async {
var timeoutCalled = false;
final pipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions(
timeout: Duration(milliseconds: 50),
onTimeout: (args) async {
timeoutCalled = true;
},
))
.build();

try {
await pipeline.execute((context) async {
await Future.delayed(Duration(milliseconds: 100));
return 'test';
});
} catch (e) {}

expect(timeoutCalled, isTrue);
});

test('should use dynamic timeout from generator', () async {
final pipeline = ResiliencePipelineBuilder()
.addTimeoutWithOptions(TimeoutStrategyOptions.withGenerator(
timeoutGenerator: (args) async {
final operationKey = args.context.operationKey ?? '';
return operationKey == 'fast'
? Duration(milliseconds: 50)
: Duration(milliseconds: 200);
},
))
.build();

// Fast operation should timeout
expect(
() => pipeline.execute(
(context) async {
await Future.delayed(Duration(milliseconds: 100));
return 'test';
},
context: ResilienceContext(operationKey: 'fast'),
),
throwsA(isA<TimeoutRejectedException>()),
);

// Slow operation should succeed
final result = await pipeline.execute(
(context) async {
await Future.delayed(Duration(milliseconds: 100));
return 'success';
},
context: ResilienceContext(operationKey: 'slow'),
);

expect(result, equals('success'));
});
});
}

Best Practices

✅ Do

  • Set reasonable timeouts based on operation characteristics
  • Use different timeouts for different types of operations
  • Monitor timeout patterns to optimize durations
  • Combine with retry strategies for comprehensive resilience
  • Handle timeouts gracefully with fallback responses
  • Consider user experience when setting UI-facing timeouts

❌ Don't

  • Set overly aggressive timeouts that cause false failures
  • Use the same timeout for all operations
  • Ignore timeout exceptions without proper handling
  • Set timeouts too long for user-facing operations
  • Forget about variable network conditions in mobile apps

Timeout Guidelines by Operation Type

Operation TypeRecommended TimeoutConsiderations
Authentication3-5 secondsUsers expect fast auth
Data Fetching10-30 secondsBalance UX and reliability
File Upload1-5 minutesBased on file size
Database Queries5-30 secondsDepends on complexity
Report Generation2-10 minutesLong-running processes
Real-time Operations1-3 secondsMust be responsive
Batch Processing5-30 minutesLarge data operations

Common Patterns

Tiered Timeout Pattern

class TieredTimeoutService {
static final _fastPipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(seconds: 5))
.build();

static final _normalPipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(seconds: 30))
.build();

static final _slowPipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(minutes: 2))
.build();

Future<T> executeFast<T>(Future<T> Function() operation) {
return _fastPipeline.execute((context) => operation());
}

Future<T> executeNormal<T>(Future<T> Function() operation) {
return _normalPipeline.execute((context) => operation());
}

Future<T> executeSlow<T>(Future<T> Function() operation) {
return _slowPipeline.execute((context) => operation());
}
}

Environment-Specific Timeouts

class EnvironmentAwareTimeouts {
static Duration getTimeout(String operationType) {
final isDevelopment = Platform.environment['ENVIRONMENT'] == 'development';
final isProduction = Platform.environment['ENVIRONMENT'] == 'production';

final timeouts = {
'api_call': isDevelopment ? Duration(minutes: 5) : Duration(seconds: 30),
'database': isDevelopment ? Duration(minutes: 2) : Duration(seconds: 15),
'file_io': Duration(seconds: 10), // Same across environments
};

return timeouts[operationType] ?? Duration(seconds: 30);
}
}

Next Steps

Timeout strategies work best when combined with other resilience patterns:

  1. 🔄 Learn Retry Strategy - Handle transient failures
  2. 🎯 Explore Fallback Strategy - Provide alternatives on timeout
  3. 🔧 Combine Strategies - Build comprehensive resilience

Timeouts are a fundamental building block of resilient systems, providing the foundation for responsive and reliable applications.