Troubleshooting Guide
Comprehensive troubleshooting guide for identifying and resolving issues with FlowCampaign email campaign platform.
Quick Diagnosis
Symptoms Index
| Symptom | Likely Cause | Immediate Action |
|---|---|---|
| Emails not sending | Provider issues, rate limiting | Check provider status, test connection |
| Low open rates | Poor subject lines, list quality | A/B test subject lines, clean list |
| High bounce rates | Invalid emails, domain issues | Clean contact list, verify sender domain |
| Slow dashboard | Database issues, high load | Check database performance, optimize queries |
| API errors | Authentication, rate limits | Verify API key, check rate limits |
| Template issues | HTML errors, CSS conflicts | Validate 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:
- Immediate: Switch to backup email provider
- 30 minutes: Contact primary provider support
- 1 hour: Notify customers of delivery delays
- 2 hours: Implement manual sending via SMTP
- 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:
- Immediate: Restart database service
- 15 minutes: Check connection pooling
- 30 minutes: Restore from recent backup if needed
- 1 hour: Implement read-only mode if necessary
- 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:
- Emergency: emergency@nssoftwaresolutions.in (24/7 for critical issues)
- Technical Support: support@nssoftwaresolutions.in
- Community Forum: forum.nssoftwaresolutions.in
- Documentation: docs.nssoftwaresolutions.in
Escalation Path:
- Check documentation and troubleshooting guide
- Search community forum for similar issues
- Contact technical support via email
- 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.