This guide explains how dead-letter processing works in hookrouter-spring, how to enable persistent storage, and how to reprocess failed notifications safely.
| Condition | DeadLetterHandler used | Storage | Reprocess support |
|---|---|---|---|
hookrouter.dead-letter.enabled=false |
no dead-letter handler bean is created | no | no |
enabled=true, custom DeadLetterHandler bean exists |
custom handler | depends on custom handler | depends on custom handler |
enabled=true, DeadLetterStore bean exists, no custom handler |
StoringDeadLetterHandler |
yes (DeadLetterStore) |
yes (DeadLetterReprocessor) |
enabled=true, no DeadLetterStore, no custom handler |
LoggingDeadLetterHandler |
no | no |
Notes:
DeadLetterReprocessoris auto-created only when aDeadLetterStorebean exists.DeadLetterScheduleris auto-created only when:DeadLetterReprocessorexistshookrouter.dead-letter.scheduler-enabled=true
- Reprocess marks an item
RESOLVEDonly when replay delivery actually succeeds. - When
enabled=truebut noDeadLetterStoreis configured, startup emits a warning and usesLoggingDeadLetterHandler(log-only mode).
hookrouter:
dead-letter:
enabled: true
max-retries: 3
scheduler-enabled: false
scheduler-interval: 60000
scheduler-batch-size: 50Key meanings:
enabled: enables dead-letter handling path.max-retries: desired maximum reprocess retries.scheduler-enabled: enables periodic reprocessing.scheduler-interval: reprocess interval in milliseconds.scheduler-batch-size: max items per scheduler run.
If you want actual storage and replay, register a DeadLetterStore bean.
import io.github.limehee.hookrouter.spring.config.WebhookConfigProperties;
import io.github.limehee.hookrouter.spring.deadletter.DeadLetterStore;
import io.github.limehee.hookrouter.spring.deadletter.InMemoryDeadLetterStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DeadLetterConfig {
@Bean
public DeadLetterStore deadLetterStore(WebhookConfigProperties properties) {
int maxRetries = Math.max(properties.getDeadLetter().getMaxRetries(), 1);
return new InMemoryDeadLetterStore(InMemoryDeadLetterStore.DEFAULT_MAX_SIZE, maxRetries);
}
}Why this matters:
InMemoryDeadLetterStoreconstructor controlsmaxRetries.- Wiring it from
hookrouter.dead-letter.max-retrieskeeps runtime behavior aligned with YAML. - When in-memory storage reaches capacity, it evicts oldest
RESOLVED/ABANDONEDfirst, then oldest remaining entry.
Enable scheduler to replay pending entries automatically:
hookrouter:
dead-letter:
enabled: true
scheduler-enabled: true
scheduler-interval: 60000
scheduler-batch-size: 100You can expose manual replay controls from your application service layer:
import io.github.limehee.hookrouter.spring.deadletter.DeadLetterReprocessor;
import io.github.limehee.hookrouter.spring.deadletter.DeadLetterStore;
import io.github.limehee.hookrouter.spring.deadletter.DeadLetterStore.DeadLetterStatus;
import org.springframework.stereotype.Service;
@Service
public class DeadLetterOperationsService {
private final DeadLetterStore store;
private final DeadLetterReprocessor reprocessor;
public DeadLetterOperationsService(DeadLetterStore store, DeadLetterReprocessor reprocessor) {
this.store = store;
this.reprocessor = reprocessor;
}
public long pendingCount() {
return store.countByStatus(DeadLetterStatus.PENDING);
}
public DeadLetterReprocessor.ReprocessSummary reprocessPending(int batchSize) {
return reprocessor.reprocessPending(batchSize);
}
public DeadLetterReprocessor.ReprocessResult reprocessById(String id) {
return reprocessor.reprocessById(id);
}
}DeadLetterHandler.FailureReason values:
MAX_RETRIES_EXCEEDEDNON_RETRYABLE_ERRORCIRCUIT_OPENRATE_LIMITEDBULKHEAD_FULLEXCEPTIONFORMATTER_NOT_FOUNDPAYLOAD_CREATION_FAILEDSENDER_NOT_FOUND
- For production, do not rely on logging-only mode; register a persistent
DeadLetterStore. - Expose dead-letter status metrics or API by status (
PENDING,PROCESSING,RESOLVED,ABANDONED). - Configure scheduler interval and batch size based on downstream webhook capacity.
- Add retention cleanup for old resolved/abandoned entries in persistent storage.