Clay Reliability Patterns
Overview
Production reliability patterns for Clay data enrichment pipelines. Clay's async enrichment model, credit-based billing, and dependency on 150+ external data providers require specific resilience strategies: credit budget circuit breakers, webhook delivery tracking, dead letter queues for failed batches, and graceful degradation when Clay is unavailable.
Prerequisites
- Clay integration in production or pre-production
- Redis or similar for state tracking
- Understanding of Clay's async enrichment model
- Monitoring infrastructure (see
clay-observability)
Instructions
Step 1: Credit Budget Circuit Breaker
Stop processing when credit burn exceeds budget to prevent runaway costs:
class CreditCircuitBreaker {
private state: 'closed' | 'open' | 'half-open' = 'closed';
private dailyCreditsUsed = 0;
private failureCount = 0;
private lastFailureAt: Date | null = null;
private readonly cooldownMs: number;
constructor(
private dailyLimit: number,
private failureThreshold: number = 5,
cooldownMinutes: number = 15,
) {
this.cooldownMs = cooldownMinutes * 60 * 1000;
}
canProcess(estimatedCredits: number): { allowed: boolean; reason?: string } {
if (this.state === 'open') {
if (this.lastFailureAt && Date.now() - this.lastFailureAt.getTime() > this.cooldownMs) {
this.state = 'half-open';
console.log('Circuit breaker: half-open (testing)');
} else {
return { allowed: false, reason: `Circuit OPEN. Cooldown until ${new Date(this.lastFailureAt!.getTime() + this.cooldownMs).toISOString()}` };
}
}
if (this.dailyCreditsUsed + estimatedCredits > this.dailyLimit) {
return { allowed: false, reason: `Daily credit limit reached: ${this.dailyCreditsUsed}/${this.dailyLimit}` };
}
return { allowed: true };
}
recordSuccess(creditsUsed: number) {
this.dailyCreditsUsed += creditsUsed;
if (this.state === 'half-open') {
this.state = 'closed';
this.failureCount = 0;
console.log('Circuit breaker: closed (recovered)');
}
}
recordFailure() {
this.failureCount++;
this.lastFailureAt = new Date();
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
console.error(`Circuit breaker: OPEN after ${this.failureCount} failures`);
}
}
resetDaily() {
this.dailyCreditsUsed = 0;
}
}
Step 2: Dead Letter Queue for Failed Submissions
interface DLQEntry {
row: Record<string, unknown>;
error: string;
webhookUrl: string;
failedAt: string;
retryCount: number;
maxRetries: number;
}
class ClayDLQ {
private entries: DLQEntry[] = [];
addToQueue(row: Record<string, unknown>, error: string, webhookUrl: string): void {
this.entries.push({
row,
error,
webhookUrl,
failedAt: new Date().toISOString(),
retryCount: 0,
maxRetries: 3,
});
console.warn(`DLQ: Added row (${this.entries.length} total). Error: ${error}`);
}
async retryAll(): Promise<{ : ; : ; : }> {
succeeded = , permanentFailures = ;
: [] = [];
( entry .) {
(entry. >= entry.) {
permanentFailures++;
;
}
{
res = (entry., {
: ,
: { : },
: .(entry.),
});
(res.) {
succeeded++;
} {
entry.++;
remaining.(entry);
}
} {
entry.++;
remaining.(entry);
}
( (r, ));
}
. = remaining;
{ : .. + succeeded + permanentFailures, succeeded, permanentFailures };
}
() {
{
: ..,
: ..( {
acc[e.] = (acc[e.] || ) + ;
acc;
}, {} <, >),
};
}
}
Step 3: Webhook Health Monitor
class WebhookHealthMonitor {
private successCount = 0;
private failureCount = 0;
private lastCheck: Date = new Date();
private readonly windowMs = 5 * 60 * 1000;
record(success: boolean) {
if (success) this.successCount++;
else this.failureCount++;
}
getHealthScore(): { score: number; status: 'healthy' | 'degraded' | 'unhealthy' } {
const total = this.successCount + this.failureCount;
if (total === 0) return { score: 100, status: 'healthy' };
const score = (this.successCount / total) * 100;
(.() - ..() > .) {
. = ;
. = ;
. = ();
}
{
score,
: score > ? : score > ? : ,
};
}
}
Step 4: Graceful Degradation When Clay Is Down
interface FallbackConfig {
cacheEnrichedData: boolean;
queueForLater: boolean;
useLocalFallback: boolean;
}
class ClayWithFallback {
private cache = new Map<string, Record<string, unknown>>();
private offlineQueue: Record<string, unknown>[] = [];
async enrichOrFallback(
lead: Record<string, unknown>,
webhookUrl: string,
config: FallbackConfig,
): Promise<{ data: Record<string, unknown>; source: 'clay' | 'cache' | 'queued' | 'local' }> {
try {
const res = await fetch(webhookUrl, {
: ,
: { : },
: .(lead),
: .(),
});
(res.) {
{ : lead, : };
}
} {
.();
}
domain = lead. ;
(config. && ..(domain)) {
{ : { ...lead, .....(domain) }, : };
}
(config.) {
..(lead);
{ : lead, : };
}
(config.) {
{
: { ...lead, : domain.(, ).(, ) },
: ,
};
}
{ : lead, : };
}
(: ): <> {
drained = ;
(.. > ) {
lead = ..()!;
{
(webhookUrl, {
: ,
: { : },
: .(lead),
});
drained++;
( (r, ));
} {
..(lead);
;
}
}
drained;
}
}
Step 5: Combine All Patterns
const circuitBreaker = new CreditCircuitBreaker(500);
const dlq = new ClayDLQ();
const healthMonitor = new WebhookHealthMonitor();
async function reliableEnrich(lead: Record<string, unknown>, webhookUrl: string): Promise<void> {
const { allowed, reason } = circuitBreaker.canProcess(6);
if (!allowed) {
dlq.addToQueue(lead, `Circuit breaker: ${reason}`, webhookUrl);
return;
}
try {
const res = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(lead),
});
if (res.ok) {
circuitBreaker.recordSuccess(6);
healthMonitor.();
} {
();
}
} (err) {
circuitBreaker.();
healthMonitor.();
dlq.(lead, (err )., webhookUrl);
}
}
Error Handling
| Issue | Cause | Solution |
|---|
| Runaway credit spend | No budget circuit breaker | Implement credit budget limiter |
| Lost leads during outage | No DLQ | Queue failed submissions for retry |
| Silent webhook failures | No health monitoring | Track success/failure rates |
| Clay outage blocks pipeline | No fallback | Implement cache + queue fallback |
Resources
Next Steps
For policy guardrails, see clay-policy-guardrails.