Skip to main content

Troubleshooting Guide

Comprehensive troubleshooting guide for identifying and resolving issues with FlowCampaign email campaign platform.

Quick Diagnosis

Symptoms Index

SymptomLikely CauseImmediate Action
Emails not sendingProvider issues, rate limitingCheck provider status, test connection
Low open ratesPoor subject lines, list qualityA/B test subject lines, clean list
High bounce ratesInvalid emails, domain issuesClean contact list, verify sender domain
Slow dashboardDatabase issues, high loadCheck database performance, optimize queries
API errorsAuthentication, rate limitsVerify API key, check rate limits
Template issuesHTML errors, CSS conflictsValidate HTML, test in multiple clients

Common Issues & Solutions

Email Delivery Issues

1. Emails Not Sending

Symptoms:

  • Campaign stuck in "sending" state
  • No delivery reports
  • Error messages in logs

Diagnosis:

# Check provider status
npm run test:providers

# Check logs
tail -f logs/email-delivery.log

Solutions:

A. Provider Connection Issues
// Test provider connection
const testResult = await emailService.testProvider({
provider: 'zeptomail',
apiKey: process.env.ZEPTOMAIL_API_KEY
});

if (!testResult.success) {
// Check API key
console.log('Invalid API key or provider issue');

// Try alternative provider
await emailService.switchToFallbackProvider();
}
B. Rate Limiting
{
"rate_limit_status": {
"current_usage": 950,
"limit": 1000,
"reset_in": "15 minutes",
"action": "Wait for reset or upgrade plan"
}
}
C. DNS Configuration Issues
# Check DNS records
dig yourdomain.com TXT
dig _dmarc.yourdomain.com TXT
dig default._domainkey.yourdomain.com TXT

# Verify SPF record includes all sending IPs

2. Emails Going to Spam

Symptoms:

  • High spam placement rates
  • Deliverability warnings
  • Feedback loop complaints

Diagnosis:

# Check spam score
npm run test:spam -- --email campaign.html

# Check authentication
npm run test:authentication

Solutions:

A. Improve Authentication
# Update DNS records for better authentication:
# SPF: Include all sending servers
# DKIM: Generate and publish key
# DMARC: Set policy to monitor or reject

# Example SPF record
"v=spf1 include:_spf.zeptomail.com include:_spf.google.com ~all"
B. Content Optimization
// Avoid spam trigger words
const spamTriggers = [
'free', 'guaranteed', 'winner', 'prize',
'cash', 'money back', 'no cost', 'urgent'
];

function checkSpamWords(content) {
return spamTriggers.filter(word =>
content.toLowerCase().includes(word)
);
}
C. List Hygiene
-- Clean inactive contacts
DELETE FROM contacts
WHERE last_engaged < NOW() - INTERVAL '6 months'
AND status = 'active';

-- Remove hard bounces immediately
DELETE FROM contacts
WHERE status = 'bounced_hard';

3. High Bounce Rates

Symptoms:

  • Delivery rate below 95%
  • Many hard/soft bounces
  • ISP blocks or throttling

Diagnosis:

-- Analyze bounce patterns
SELECT
bounce_type,
COUNT(*) as count,
AVG(retry_count) as avg_retries,
MIN(bounced_at) as first_bounce,
MAX(bounced_at) as last_bounce
FROM email_events
WHERE event_type = 'bounced'
GROUP BY bounce_type;

Solutions:

A. Clean Contact List
// Automated list cleaning
async function cleanContactList(listId) {
const contacts = await getContacts(listId);

const invalidEmails = contacts.filter(contact =>
!isValidEmail(contact.email)
);

const disposableEmails = contacts.filter(contact =>
isDisposableEmail(contact.email)
);

// Mark for removal
await markForRemoval([...invalidEmails, ...disposableEmails]);

return {
cleaned: invalidEmails.length + disposableEmails.length,
remaining: contacts.length - (invalidEmails.length + disposableEmails.length)
};
}
B. Implement Double Opt-in
// Double opt-in process
async function doubleOptIn(email) {
// Send confirmation email
const token = generateVerificationToken();
await sendConfirmationEmail(email, token);

// Wait for confirmation
const confirmed = await waitForConfirmation(token, 48 * 60 * 60 * 1000);

if (confirmed) {
await addToVerifiedList(email);
return { success: true, message: 'Confirmed' };
}

return { success: false, message: 'Not confirmed within timeframe' };
}
C. Gradual Warm-up
{
"ip_warmup_schedule": {
"day_1": {"volume": 50, "segments": ["most_engaged"]},
"day_2": {"volume": 100, "segments": ["engaged", "active"]},
"day_3": {"volume": 200, "segments": ["all_active"]},
"day_4_7": {"volume": 500, "segments": ["all_users"]},
"week_2": {"volume": 1000, "segments": ["all_users"]}
}
}

Campaign Performance Issues

1. Low Open Rates

Symptoms:

  • Open rate below industry average (20%)
  • Poor engagement metrics
  • High unsubscribe rates

Diagnosis:

-- Analyze open patterns
SELECT
EXTRACT(HOUR FROM opened_at) as hour,
DAYNAME(opened_at) as day,
COUNT(*) as opens,
AVG(TIMESTAMPDIFF(MINUTE, sent_at, opened_at)) as avg_time_to_open
FROM email_events
WHERE event_type = 'opened'
GROUP BY hour, day
ORDER BY opens DESC;

Solutions:

A. Optimize Subject Lines
// A/B test subject lines
async function testSubjectLines(campaignId, variations) {
const results = await Promise.all(
variations.map(async (variation) => {
const testCampaign = await createTestCampaign({
...campaignData,
subject: variation
});

const metrics = await sendAndTrack(testCampaign, 1000);

return {
subject: variation,
open_rate: metrics.open_rate,
click_rate: metrics.click_rate
};
})
);

return results.sort((a, b) => b.open_rate - a.open_rate);
}
B. Improve Send Times
// Find optimal send times
async function findOptimalSendTime(contactListId) {
const engagementData = await getEngagementPatterns(contactListId);

// Analyze by timezone
const timezoneAnalysis = engagementData.reduce((acc, data) => {
const hour = data.local_hour;
acc[hour] = (acc[hour] || 0) + 1;
return acc;
}, {});

// Find peak hours
const optimalHours = Object.entries(timezoneAnalysis)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([hour]) => parseInt(hour));

return optimalHours;
}
C. Segment Effectively
-- Create engagement-based segments
CREATE TEMPORARY TABLE engagement_segments AS
SELECT
contact_id,
CASE
WHEN open_rate > 0.3 THEN 'high_engagement'
WHEN open_rate > 0.15 THEN 'medium_engagement'
ELSE 'low_engagement'
END as segment
FROM (
SELECT
contact_id,
COUNT(CASE WHEN event_type = 'opened' THEN 1 END)::float /
COUNT(CASE WHEN event_type = 'sent' THEN 1 END) as open_rate
FROM email_events
GROUP BY contact_id
) engagement;

2. Low Click Rates

Symptoms:

  • Click-through rate below 2%
  • Poor conversion rates
  • High exit rates from landing pages

Diagnosis:

-- Analyze click patterns
SELECT
link_url,
COUNT(*) as clicks,
COUNT(DISTINCT contact_id) as unique_clicks,
AVG(TIMESTAMPDIFF(SECOND, opened_at, clicked_at)) as avg_time_to_click
FROM email_events
WHERE event_type = 'clicked'
GROUP BY link_url
ORDER BY clicks DESC;

Solutions:

A. Improve Call-to-Action
<!-- Effective CTAs -->
<a href="{{cta_url}}"
style="display: inline-block;
padding: 12px 24px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
font-weight: bold;">
🚀 Start Free Trial
</a>

<!-- vs. Poor CTA -->
<a href="{{cta_url}}">Click here</a>
B. Personalize Content
// Dynamic content based on user data
function personalizeContent(user, template) {
return template
.replace('{{first_name}}', user.first_name || 'there')
.replace('{{company}}', user.company || '')
.replace('{{location}}', user.location || 'your area')
.replace('{{last_purchase}}', formatDate(user.last_purchase) || 'recently');
}
C. Optimize Landing Pages
// Test landing page effectiveness
async function testLandingPageVariations(landingPages) {
const results = await Promise.all(
landingPages.map(async (page) => {
const conversionRate = await trackConversions(page.url);
return {
url: page.url,
conversion_rate: conversionRate,
load_time: await measureLoadTime(page.url)
};
})
);

return results.sort((a, b) => b.conversion_rate - a.conversion_rate);
}

Technical Issues

1. Slow Dashboard Performance

Symptoms:

  • Dashboard loads slowly (> 3 seconds)
  • Charts and graphs delayed
  • Timeout errors on large datasets

Diagnosis:

-- Check slow queries
EXPLAIN ANALYZE
SELECT * FROM campaigns
WHERE status = 'sent'
ORDER BY sent_at DESC
LIMIT 100;

-- Check table sizes
SELECT
table_name,
pg_size_pretty(pg_total_relation_size(table_name)) as size
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY pg_total_relation_size(table_name) DESC;

Solutions:

A. Database Optimization
-- Add indexes for common queries
CREATE INDEX idx_campaigns_status_sent_at
ON campaigns(status, sent_at DESC);

CREATE INDEX idx_email_events_campaign_id
ON email_events(campaign_id, event_type, event_time);

-- Implement materialized views for analytics
CREATE MATERIALIZED VIEW campaign_daily_stats AS
SELECT
DATE(sent_at) as date,
campaign_id,
COUNT(*) as sent,
COUNT(CASE WHEN event_type = 'opened' THEN 1 END) as opened,
COUNT(CASE WHEN event_type = 'clicked' THEN 1 END) as clicked
FROM email_events
WHERE sent_at > NOW() - INTERVAL '30 days'
GROUP BY DATE(sent_at), campaign_id;
B. Implement Caching
// Redis caching for dashboard data
const cacheDashboardData = async (userId, timeframe) => {
const cacheKey = `dashboard:${userId}:${timeframe}`;

// Try cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}

// Generate fresh data
const data = await generateDashboardData(userId, timeframe);

// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(data));

return data;
};
C. Paginate Large Datasets
// Implement cursor-based pagination
async function getPaginatedCampaigns(cursor, limit = 50) {
const query = `
SELECT * FROM campaigns
WHERE ${cursor ? `id < $2 AND` : ''}
status = 'sent'
ORDER BY id DESC
LIMIT $1
`;

const params = cursor ? [limit, cursor] : [limit];
const results = await db.query(query, params);

return {
data: results,
next_cursor: results.length > 0 ? results[results.length - 1].id : null
};
}

2. API Rate Limiting Errors

Symptoms:

  • HTTP 429 errors
  • "Rate limit exceeded" messages
  • Inconsistent API responses

Diagnosis:

// Check rate limit headers
const checkRateLimits = (headers) => {
return {
remaining: headers['x-ratelimit-remaining'],
limit: headers['x-ratelimit-limit'],
reset: headers['x-ratelimit-reset'],
retryAfter: headers['retry-after']
};
};

Solutions:

A. Implement Retry Logic
// Exponential backoff with retry
async function makeRequestWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);

if (response.status === 429) {
const retryAfter = response.headers.get('retry-after') || Math.pow(2, i);
await sleep(retryAfter * 1000);
continue;
}

return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
await sleep(Math.pow(2, i) * 1000);
}
}
}
B. Batch Operations
// Batch API calls to reduce rate limit impact
async function batchSendEmails(emails, batchSize = 100) {
const batches = [];

for (let i = 0; i < emails.length; i += batchSize) {
batches.push(emails.slice(i, i + batchSize));
}

const results = [];

for (const batch of batches) {
const batchResult = await sendEmailBatch(batch);
results.push(...batchResult);

// Delay between batches
if (batches.indexOf(batch) < batches.length - 1) {
await sleep(1000);
}
}

return results;
}
C. Monitor Usage
// Track API usage
class RateLimitMonitor {
constructor(limit, windowMs) {
this.limit = limit;
this.windowMs = windowMs;
this.requests = [];
}

addRequest() {
const now = Date.now();
this.requests.push(now);

// Remove old requests
this.requests = this.requests.filter(
timestamp => now - timestamp < this.windowMs
);

return this.requests.length;
}

getRemaining() {
return this.limit - this.requests.length;
}

getResetTime() {
if (this.requests.length === 0) return 0;
const oldest = this.requests[0];
return oldest + this.windowMs - Date.now();
}
}

Integration Issues

1. Webhook Failures

Symptoms:

  • Webhook deliveries failing
  • Missing event notifications
  • Inconsistent webhook payloads

Diagnosis:

// Monitor webhook deliveries
const webhookMonitor = {
failures: [],

async deliverWebhook(url, payload) {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-FlowCampaign-Signature': generateSignature(payload)
},
body: JSON.stringify(payload),
timeout: 5000
});

if (!response.ok) {
this.logFailure(url, payload, response.status);
return false;
}

return true;
} catch (error) {
this.logFailure(url, payload, error.message);
return false;
}
},

logFailure(url, payload, error) {
this.failures.push({
url,
payload: JSON.stringify(payload).substring(0, 100),
error,
timestamp: new Date().toISOString()
});

// Keep only recent failures
if (this.failures.length > 100) {
this.failures.shift();
}
}
};

Solutions:

A. Implement Retry Queue
// Webhook retry queue
class WebhookRetryQueue {
constructor(maxRetries = 3, retryDelay = 5000) {
this.queue = [];
this.maxRetries = maxRetries;
this.retryDelay = retryDelay;
}

async addWebhook(url, payload, attempt = 0) {
if (attempt >= this.maxRetries) {
console.error(`Webhook failed after ${this.maxRetries} attempts`, { url, payload });
return;
}

try {
await this.deliverWebhook(url, payload);
} catch (error) {
// Schedule retry
setTimeout(() => {
this.addWebhook(url, payload, attempt + 1);
}, this.retryDelay * Math.pow(2, attempt));
}
}

async deliverWebhook(url, payload) {
// Implementation with timeout and validation
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);

const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-FlowCampaign-Signature': generateSignature(payload)
},
body: JSON.stringify(payload),
signal: controller.signal
});

clearTimeout(timeoutId);

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return response;
}
}
B. Add Webhook Validation
// Validate webhook endpoints before use
async function validateWebhookEndpoint(url) {
const testPayload = {
event: 'test',
timestamp: new Date().toISOString(),
data: { test: true }
};

try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-FlowCampaign-Test': 'true'
},
body: JSON.stringify(testPayload),
timeout: 10000
});

return {
valid: response.ok,
status: response.status,
latency: Date.now() - startTime
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}

2. Template Rendering Issues

Symptoms:

  • Emails look broken in certain clients
  • CSS not applying correctly
  • Images not displaying

Diagnosis:

<!-- Test template in multiple clients -->
<!DOCTYPE html>
<html>
<head>
<style>
/* Test various CSS properties */
.test { color: red; }
@media only screen and (max-width: 600px) {
.mobile { display: block; }
}
</style>
</head>
<body>
<!-- Test different HTML elements -->
<div class="test">CSS Test</div>
<img src="https://via.placeholder.com/100" alt="Image Test">
<table><tr><td>Table Test</td></tr></table>
</body>
</html>

Solutions:

A. Email Client Testing
// Test template in multiple email clients
async function testTemplateCompatibility(templateHtml) {
const clients = [
{ name: 'Gmail', renderer: 'gmail' },
{ name: 'Outlook', renderer: 'outlook' },
{ name: 'Apple Mail', renderer: 'apple' },
{ name: 'Yahoo', renderer: 'yahoo' }
];

const results = await Promise.all(
clients.map(async (client) => {
const rendered = await renderForClient(templateHtml, client.renderer);
return {
client: client.name,
supported: checkSupport(rendered),
warnings: getWarnings(rendered),
screenshot: await takeScreenshot(rendered)
};
})
);

return results;
}
B. Use Inline CSS
<!-- Convert CSS to inline -->
<style>
.button {
background-color: #007bff;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 4px;
}
</style>

<!-- Becomes -->
<a href="#" style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px;">
Click Me
</a>
C. Fallback Content
<!-- Provide fallbacks for unsupported features -->
<!--[if mso]>
<!-- Outlook-specific HTML -->
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="background-color: #007bff; padding: 12px 24px;">
<a href="#" style="color: white; text-decoration: none;">Click Me</a>
</td>
</tr>
</table>
<![endif]-->

<!--[if !mso]><!-->
<!-- Standard HTML for other clients -->
<a href="#" class="button">Click Me</a>
<!--<![endif]-->

Emergency Procedures

Service Outage Response

1. Email Delivery Stopped

Immediate Actions:

# 1. Check provider status
npm run check:providers

# 2. Switch to fallback provider
npm run switch:fallback

# 3. Check queue status
npm run check:queue

# 4. Review error logs
tail -f logs/error.log | grep -i "email\|provider"

Escalation Steps:

  1. Immediate: Switch to backup email provider
  2. 30 minutes: Contact primary provider support
  3. 1 hour: Notify customers of delivery delays
  4. 2 hours: Implement manual sending via SMTP
  5. 4 hours: Full incident report and root cause analysis

2. Database Connection Lost

Immediate Actions:

# 1. Check database status
systemctl status postgresql

# 2. Check disk space
df -h

# 3. Check logs
tail -f /var/log/postgresql/postgresql-14-main.log

# 4. Restart database
sudo systemctl restart postgresql

Recovery Steps:

  1. Immediate: Restart database service
  2. 15 minutes: Check connection pooling
  3. 30 minutes: Restore from recent backup if needed
  4. 1 hour: Implement read-only mode if necessary
  5. 2 hours: Full database recovery plan

Data Recovery

1. Accidental Data Deletion

Recovery Procedure:

# 1. Stop writes to database
npm run maintenance:enable

# 2. Restore from latest backup
psql -U flowcampaign_user -d flowcampaign < latest_backup.sql

# 3. Apply transaction logs (if using WAL)
pg_archivecleanup /var/lib/postgresql/wal_archive

# 4. Verify data integrity
npm run db:verify

2. Corruption Recovery

# 1. Enter single-user mode
pg_ctl -D /var/lib/postgresql/data stop
postgres --single -D /var/lib/postgresql/data flowcampaign

# 2. Repair corrupted tables
REINDEX DATABASE flowcampaign;
VACUUM FULL ANALYZE;

# 3. Check for hardware issues
smartctl -a /dev/sda

Performance Optimization

Query Optimization

Slow Query Identification

-- Find slow queries
SELECT
query,
calls,
total_time,
mean_time,
rows
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

Index Optimization

-- Add missing indexes
CREATE INDEX CONCURRENTLY idx_email_events_composite
ON email_events(campaign_id, event_type, event_time);

-- Analyze index usage
SELECT
schemaname,
tablename,
indexname,
idx_scan as scans,
idx_tup_read as rows_read,
idx_tup_fetch as rows_fetched
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;

Caching Strategy

Application-Level Caching

// Multi-layer caching strategy
const cachingStrategy = {
layer1: {
type: 'memory',
ttl: 60, // seconds
maxSize: 1000
},
layer2: {
type: 'redis',
ttl: 300, // seconds
compression: true
},
layer3: {
type: 'database',
materialized: true,
refresh: 'daily'
}
};

Monitoring & Alerting

Health Check Endpoints

// Comprehensive health check
app.get('/health', async (req, res) => {
const checks = {
database: await checkDatabase(),
redis: await checkRedis(),
email_providers: await checkEmailProviders(),
disk_space: await checkDiskSpace(),
memory: await checkMemory(),
uptime: process.uptime()
};

const allHealthy = Object.values(checks).every(check => check.healthy);
const status = allHealthy ? 200 : 503;

res.status(status).json({
status: allHealthy ? 'healthy' : 'unhealthy',
checks,
timestamp: new Date().toISOString()
});
});

Alert Configuration

# Alerting rules configuration
alerts:
critical:
- condition: "delivery_rate < 90% for 15 minutes"
channels: ["pagerduty", "slack_critical"]

- condition: "database_connections > 90%"
channels: ["email_admin", "slack_critical"]

warning:
- condition: "open_rate < 15%"
channels: ["slack_marketing"]

- condition: "api_errors > 1%"
channels: ["slack_engineering"]

- condition: "disk_usage > 80%"
channels: ["slack_infrastructure"]

Support Channels:

Escalation Path:

  1. Check documentation and troubleshooting guide
  2. Search community forum for similar issues
  3. Contact technical support via email
  4. Emergency contact for critical production issues

This troubleshooting guide provides comprehensive solutions for common issues. For persistent or complex problems, don't hesitate to contact our support team.