Troubleshooting EmailEngine
What to check when EmailEngine, an account, a webhook, or a submission is not behaving. The API calls below use https://emailengine.example.com for the instance and TOKEN for an access token; replace both.
- Check the health endpoint:
curl https://emailengine.example.com/health - Check the logs:
journalctl -u emailengine -n 100ordocker logs emailengine - Check Redis:
redis-cli ping - Open the account in the admin interface and read its state and last error
Quick Diagnostic Checklist
Use this checklist for initial troubleshooting:
- EmailEngine service is running
- Redis is running and accessible
- Redis has available memory
- Network connectivity to IMAP/SMTP servers
- Firewall not blocking required ports
- Valid credentials for email accounts
- OAuth2 tokens not expired
- Sufficient disk space
- System has adequate RAM
- No conflicting processes on ports
Common Issues by Category
Connection Issues
EmailEngine Won't Start
Symptom: Service fails to start or exits immediately
Diagnostic steps:
# Check if process is running
ps aux | grep emailengine
# Check logs
journalctl -u emailengine -n 50
# Or for Docker
docker logs emailengine
# Check the installed version
emailengine version
Common causes and solutions:
-
Redis connection failed
A Redis problem at startup is printed to the console in a boxed message rather than only logged, and the process exits. The wording names the cause:
Can not connect to the database. Redis might not be running. Are you using correct hostname and port values?forECONNREFUSEDConnection to the database timed out. Seems like you are firewalled. Are you using correct hostname and port values?forETIMEDOUTRedis password is required but not providedorRedis requires a valid passwordwhen the server answeredNOAUTHProvided Redis password was not acceptedwhen it answeredWRONGPASS
Anything else is logged as
Redis connection errorwith the underlying error attached; a hostname that does not resolve shows up there asENOTFOUND.Solution:
# Check Redis status
sudo systemctl status redis
sudo systemctl start redis
# Test connection
redis-cli ping
# Verify the URL EmailEngine uses
echo $EENGINE_REDIS
# Should be: redis://localhost:6379 -
Port already in use
The API listener binds to
127.0.0.1:3000by default, and a bind failure surfaces as anEADDRINUSEerror naming that address:Error: listen EADDRINUSE: address already in use 127.0.0.1:3000Solution:
# Find process using port
sudo lsof -i :3000
sudo netstat -tulpn | grep :3000
# Kill process or use different port
export EENGINE_PORT=3001EENGINE_HOSTchanges the bind address the same way. The built-in SMTP server, which starts on127.0.0.1:2525, and the IMAP proxy are separate listeners with their own port settings. -
Node.js version too old
EmailEngine declares Node.js 20 or newer in its
enginesfield. The startup guard is older than that requirement and only refuses to run below Node.js 17, exiting immediately withNode.js version vX.Y.Z is not supported. Please upgrade to Node.js 17 or later.. A version between 17 and 19 starts but is untested, so treat a strange failure on one as a version problem.Solution:
# Check version
node --version
# Update Node.js (using nvm)
nvm install 20
nvm use 20
# Or install latest LTS
nvm install --ltsThe packaged builds (Docker image, macOS installer, Linux and Windows binaries) bundle their own Node.js runtime.
Accounts Stay Disconnected
Symptom: Most or all accounts show disconnected or connectError and do not recover
Diagnostic steps:
# Check every account's state
curl https://emailengine.example.com/v1/accounts \
-H "Authorization: Bearer TOKEN" | jq '.accounts[] | {account, state}'
# Check one account
curl https://emailengine.example.com/v1/account/user@example.com \
-H "Authorization: Bearer TOKEN" | jq '{state, lastError, authFailureDisabledAt}'
# Check logs for connection errors
journalctl -u emailengine | grep -i "ECONNREFUSED\|ETIMEDOUT\|ENOTFOUND"
Common causes:
-
Redis out of memory
Check Redis memory:
redis-cli INFO memory | grep used_memory_human
redis-cli INFO memory | grep maxmemory_humanSolution: with no
maxmemoryset, Redis uses whatever the host has, so the fix is more RAM on the host. With one set, raise it in/etc/redis/redis.confor remove the directive, then restart:sudo systemctl restart redisCheck the eviction policy while you are there. EmailEngine needs everything it stores, so an
allkeys-*policy silently discards live data;noevictionis the setting to use:redis-cli CONFIG GET maxmemory-policyRedis holds every account's credentials and sync state, so
FLUSHDBis not a cleanup option; it deletes the accounts. Size the instance with Performance tuning. -
Network connectivity issues
Test IMAP connection:
# Test IMAP host reachability
telnet imap.gmail.com 993
openssl s_client -connect imap.gmail.com:993
# Test SMTP
telnet smtp.gmail.com 587Check firewall:
# Check if ports are blocked
sudo iptables -L -n | grep -E "993|587|465|143"
# Allow IMAP/SMTP ports
sudo ufw allow out 993/tcp
sudo ufw allow out 587/tcpFor a complete list of domains and ports EmailEngine needs to reach, see Outbound Connection Whitelist.
-
Rate limiting by the email provider
Providers limit simultaneous IMAP connections per mailbox and per source address. Spread accounts across instances or egress addresses, and route the affected accounts through a proxy with the account-level
proxyfield:{
"proxy": "socks5://proxy.example.com:1080"
}See Proxying connections.
-
Too many accounts for the worker count
# Increase IMAP worker threads
export EENGINE_WORKERS=8
# Check resource usage
top -p $(pgrep -f emailengine | head -1)
Account Switched Off After Authentication Failures
Symptom: An account shows unset in the API and a Syncing switched off badge in the accounts list, and PUT /v1/account/{account}/reconnect returns {"reconnect": false}
When credentials keep being rejected for longer than EENGINE_MAX_IMAP_AUTH_FAILURE_TIME (3 days by default), EmailEngine stops connecting the account rather than retrying a dead password or grant forever.
Diagnostic:
curl https://emailengine.example.com/v1/account/user@example.com \
-H "Authorization: Bearer TOKEN" | jq '{state, authFailureDisabledAt, disabled: .imap.disabled}'
authFailureDisabledAt (since v2.79.4) is the time EmailEngine switched the account off. It is null when the operator disabled the account deliberately, in which case imap.disabled is the setting to clear. To find every parked account at once, the same field is on each entry of GET /v1/accounts.
Solution: supply working credentials, or resume with the stored ones. Re-authorizing an OAuth2 account, saving new IMAP settings, or Resume syncing on the account page all lift it; which forms of PUT /v1/account/{account} count has changed between releases. Accounts switched off after authentication failures is the canonical description of the mechanism, the recovery paths and the per-version differences, and EENGINE_MAX_IMAP_AUTH_FAILURE_TIME is the setting behind the window.
IMAP Connection Timeouts
Symptom: Accounts connect but frequently time out
Diagnostic:
# Enable protocol logging (writes the raw IMAP conversation, credentials included)
export EENGINE_LOG_RAW=true
export EENGINE_LOG_LEVEL=trace
# Check logs for timeouts
journalctl -u emailengine | grep -i timeout
# Measure network latency
ping imap.gmail.com
traceroute imap.gmail.com
Solutions:
-
Raise the IMAP socket timeout:
# Milliseconds or a duration string; unset by default
export EENGINE_IMAP_SOCKET_TIMEOUT=120s -
Check network quality:
# Test packet loss
mtr -c 100 imap.gmail.com
# Check firewall interference
sudo iptables -L -v -
Route through a proxy if the direct path is the problem:
{
"proxy": "socks5://proxy.example.com:1080"
}
OAuth2 Authentication Issues
OAuth2 Flow Fails
Symptom: OAuth2 authentication page shows error or redirect fails
Diagnostic:
# List OAuth2 applications
curl https://emailengine.example.com/v1/oauth2 \
-H "Authorization: Bearer TOKEN" | jq '.'
# Check one application (use the app ID from the listing)
curl https://emailengine.example.com/v1/oauth2/AAABhaBPHscAAAAH \
-H "Authorization: Bearer TOKEN" | jq '.'
# Check that the provider's token endpoint is reachable
curl -I https://oauth2.googleapis.com/token
Common causes:
-
Invalid client ID/secret
Solution:
- Verify the client ID and secret in Google Cloud Console (or the Microsoft Entra app for Outlook)
- Update the values in the EmailEngine OAuth2 application (dashboard under Integrations > OAuth2 Apps, or
PUT /v1/oauth2/{app}). OAuth2 credentials are stored on the OAuth2 application, not in environment variables - Check for trailing spaces when pasting the client ID/secret into the app form
- After updating, use the OAuth2 app's Verify setup action (
POST /v1/oauth2/{app}/verify) to confirm the credentials work
-
Incorrect redirect URI
Solution:
Set EmailEngine's service URL correctly - it is the
serviceUrlsetting (configured in the dashboard under Configuration, or via theEENGINE_SETTINGSJSON), not a standalone environment variable. The OAuth2 redirect URI is derived from it.# Provide serviceUrl through prepared settings
export EENGINE_SETTINGS='{"serviceUrl":"https://emailengine.example.com"}'
# Verify the redirect URI registered in the OAuth2 provider
# Should match: https://emailengine.example.com/oauth -
OAuth2 scopes insufficient
The scopes EmailEngine requests depend on the application's base scopes. The provider console has to allow them:
Gmail:
- IMAP backend:
https://mail.google.com/ - Gmail API backend:
https://www.googleapis.com/auth/gmail.modify
Microsoft 365 (the global cloud; the GCC High, DoD, and China clouds use their own hostnames for the same scopes):
- IMAP backend:
https://outlook.office.com/IMAP.AccessAsUser.All,https://outlook.office.com/SMTP.Send,offline_access,openid,profile - Graph API backend:
https://graph.microsoft.com/Mail.ReadWrite,https://graph.microsoft.com/Mail.Send,https://graph.microsoft.com/User.Read,offline_access
- IMAP backend:
Token Refresh Fails
Symptom: Accounts work initially but stop after token expiry
Diagnostic:
# Check token expiry
curl https://emailengine.example.com/v1/account/user@example.com \
-H "Authorization: Bearer TOKEN" | jq '.oauth2 | {expires, scope, provider}'
# Check logs for refresh errors
journalctl -u emailengine | grep -i "refresh\|invalid_grant"
Solutions:
-
Refresh token expired or revoked:
- Re-authorize the account
- An account that has been failing for three days is switched off; see Account switched off after authentication failures
-
OAuth2 app disabled:
- Verify app status in provider console
- Check for security alerts
-
Encryption secret changed:
- Tokens encrypted with the previous
EENGINE_SECRETcannot be decrypted with the new one - Restore the previous secret, or re-encrypt the data with
emailengine encryptas described in Secret encryption, before re-authorizing every account
- Tokens encrypted with the previous
Webhook Delivery Issues
Webhooks Not Delivered
Symptom: Events occur but webhooks aren't received
Diagnostic:
# Check webhook configuration
curl https://emailengine.example.com/v1/settings?webhooks=true\&webhooksEnabled=true\&webhookEvents=true \
-H "Authorization: Bearer TOKEN"
# Check webhook queue
curl https://emailengine.example.com/v1/settings/queue/notify \
-H "Authorization: Bearer TOKEN"
# Test webhook endpoint
curl -X POST https://your-app.com/webhooks \
-H "Content-Type: application/json" \
-d '{"test": true}'
# Check logs
journalctl -u emailengine | grep -i webhook
Common causes:
-
Webhook URL not set, or no events selected
webhookEventsis an allowlist with no default: with nothing selected, nothing is delivered.["*"]selects every event.Solution:
curl -X POST https://emailengine.example.com/v1/settings \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"webhooks": "https://your-app.com/webhooks", "webhooksEnabled": true, "webhookEvents": ["*"]}' -
Webhook endpoint unreachable
Test from EmailEngine server:
curl -I https://your-app.com/webhooks
# Check DNS
nslookup your-app.com
# Check firewall
telnet your-app.com 443 -
Webhook timeout
Each delivery attempt is capped at 30 seconds. A receiver that needs longer than that has to acknowledge first and do the work afterwards. If the endpoint is genuinely slow and you cannot change it, raise the cap at startup:
EENGINE_WEBHOOK_TIMEOUT=60sThe retry policy itself is fixed: 10 attempts with exponential backoff. It is not configurable.
-
TLS certificate issues
The webhook destination's certificate has to validate against the system's trust store. Check the chain the endpoint serves:
openssl s_client -connect your-app.com:443 -servername your-app.comA self-signed or incomplete chain is fixed at the endpoint; there is no setting that disables verification for webhooks.
-
Destination refused by EmailEngine
The webhook error flag on the account or configuration page shows
EEGRESSBLOCKED(the destination resolves to a blocked address, by default the link-local range used by cloud instance metadata) orEREDIRECTNOTFOLLOWED(the endpoint answered with a redirect, which is not followed).Solution: point the webhook at the endpoint's final, routable URL, or adjust
EENGINE_WEBHOOK_EGRESS_POLICY. See Blocked destinations and redirects.
Webhooks Delayed
Symptom: Webhooks delivered but with significant delay
Diagnostic:
# Check queue status
curl https://emailengine.example.com/v1/settings/queue/notify \
-H "Authorization: Bearer TOKEN" | jq
# Check backlog
redis-cli LLEN "bull:notify:wait"
# Monitor webhook processing
journalctl -u emailengine -f | grep webhook
A growing waiting count means deliveries are being produced faster than they complete, so the fix is either fewer or cheaper deliveries, or more of them at once.
Solutions:
-
Deliver in parallel:
One webhook worker processes one delivery at a time by default. Total concurrency is workers times per-worker concurrency:
# 4 webhook worker threads
EENGINE_WORKERS_WEBHOOKS=4
# each handling 2 deliveries at a time, so 8 in flight
EENGINE_NOTIFY_QC=2Raise this only as far as the receiving endpoint can absorb; a slow receiver saturates at its own rate whatever EmailEngine does. Webhook configuration has the sizing guidance.
-
Reduce the webhook payload:
- Turn off
notifyTextandnotifyAttachmentsin the webhook settings - Fetch content on demand through the API instead
- Turn off
-
Deliver fewer events:
- Narrow
webhookEventsfrom["*"]to the events the integration acts on - Check Redis latency, which is added to every queue operation
Confirm the queue is not paused at all.
GET /v1/settings/queue/notifyreportspaused; resume it with:curl -X PUT https://emailengine.example.com/v1/settings/queue/notify \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"paused": false}' - Narrow
Performance Issues
High Memory Usage
Symptom: EmailEngine consumes excessive RAM
Diagnostic:
# Check memory usage
ps aux | grep emailengine
# Check the Node.js heap through the Prometheus metrics endpoint.
# The token needs the "metrics" scope, which only the admin interface and
# `emailengine tokens issue` can grant.
curl https://emailengine.example.com/metrics \
-H "Authorization: Bearer METRICS_TOKEN" | grep heap
# Check account count
curl https://emailengine.example.com/v1/accounts \
-H "Authorization: Bearer TOKEN" | jq '.total'
Solutions:
-
Too many accounts:
Budget 1-2 MiB of Redis memory per account and provision twice that, more for very large mailboxes; see Redis sizing. Add instances or RAM when the budget is exceeded.
-
Memory growth over time:
# Update to the latest release, then restart
sudo systemctl restart emailengineInstallation has the update procedure for each install method. Report growth that persists on the current release as a GitHub issue with the
/metricsoutput.
Slow Performance
Symptom: API requests slow, UI sluggish
Diagnostic:
# Check Redis latency
redis-cli --latency
redis-cli --latency-history
# Check CPU usage
top
# Check IMAP response times
# (enable EENGINE_LOG_RAW=true and check logs)
# Test API performance
time curl https://emailengine.example.com/v1/accounts \
-H "Authorization: Bearer TOKEN"
Solutions:
-
Redis latency high:
Redis is EmailEngine's only data store, so its round-trip time is added to nearly every operation. A cross-region Redis is not a workable setup; move it onto the same host or the same LAN. Check what the current one costs:
# Average round-trip in milliseconds, sampled continuously
redis-cli --latencyLeave
tcp-keepaliveat the Redis default. Setting it to0leaves half-open connections behind after a network hiccup, which shows up as stalled accounts rather than as an error. -
Too few workers:
# One IMAP worker thread per CPU core
export EENGINE_WORKERS=cpusPerformance tuning covers how many accounts a thread can carry.
-
Slow Redis commands:
# Log every command that takes longer than 10 ms
redis-cli CONFIG SET slowlog-log-slower-than 10000
# Read the ten most recent entries back
redis-cli SLOWLOG GET 10
Email Sync Issues
Messages Not Syncing
Symptom: New emails don't appear in EmailEngine
Diagnostic:
# Check account state
curl https://emailengine.example.com/v1/account/user@example.com \
-H "Authorization: Bearer TOKEN" | jq '.state'
# Request a sync
curl -X PUT https://emailengine.example.com/v1/account/user@example.com/sync \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"sync": true}'
# Check logs
journalctl -u emailengine | grep -i "sync\|idle"
Solutions:
-
IMAP IDLE not available:
Without IDLE, changes are picked up on the periodic full resync, every
imap.resyncDelayseconds (900 by default). Lower it on the account if the delay is too long. -
Account not in a syncing state:
connectedandsyncingare the two healthy states.pausedandunsetmean no connection is maintained at all, andauthenticationErrormeans the credentials were rejected and have to be replaced.disconnectedandconnectErrorare retried with backoff, so those recover on their own if the underlying problem clears. Account states lists what each one needs. -
Folder not monitored:
- Check the account's
pathsetting, which lists the folders EmailEngine syncs;"*"means all of them - Add the folder, or set
pathto"*"
- Check the account's
Deleted Messages Re-appear
Symptom: Deleted emails come back after sync, or turn up under a different message ID
Cause: DELETE /v1/account/{account}/message/{message} moves the message to Trash rather than erasing it, and deletes it only when it is already in Trash. A message that reappears is normally the copy now sitting in Trash, which has its own ID because the ID encodes the folder and the IMAP UID.
Solution: check Trash first, then delete permanently if that is what you meant:
# Find the Trash folder
curl https://emailengine.example.com/v1/account/user123/mailboxes \
-H "Authorization: Bearer TOKEN" | jq '.mailboxes[] | select(.specialUse=="\\Trash")'
# Delete outright, wherever the message currently is (IMAP accounts only)
curl -X DELETE "https://emailengine.example.com/v1/account/user123/message/AAAAAQAACnA?force=true" \
-H "Authorization: Bearer TOKEN"
force has no effect on a Gmail API account: those always move the message to Trash.
Step-by-Step Diagnostic Procedures
Procedure 1: Complete Health Check
#!/bin/bash
echo "=== EmailEngine Health Check ==="
# 1. Check service
echo "1. Service status:"
systemctl is-active emailengine
# 2. Check Redis
echo "2. Redis status:"
redis-cli ping
# 3. Check health endpoint
echo "3. Health endpoint:"
curl -s https://emailengine.example.com/health | jq
# 4. Check memory
echo "4. Redis memory:"
redis-cli INFO memory | grep -E "used_memory_human|maxmemory_human"
# 5. Check accounts
echo "5. Account status:"
curl -s https://emailengine.example.com/v1/accounts \
-H "Authorization: Bearer TOKEN" | \
jq '[.accounts[] | {account: .account, state: .state}]'
# 6. Check logs for errors
echo "6. Recent errors:"
journalctl -u emailengine --since "5 minutes ago" | grep '"level":50'
Procedure 2: Network Connectivity Test
#!/bin/bash
echo "=== Network Connectivity Test ==="
# Test IMAP
echo "Testing IMAP (Gmail):"
timeout 5 bash -c "</dev/tcp/imap.gmail.com/993" && echo "OK" || echo "FAILED"
# Test SMTP
echo "Testing SMTP (Gmail):"
timeout 5 bash -c "</dev/tcp/smtp.gmail.com/587" && echo "OK" || echo "FAILED"
# Test Redis
echo "Testing Redis:"
redis-cli ping
# Test webhook endpoint
echo "Testing webhook:"
curl -I -s https://your-app.com/webhooks | head -1
Procedure 3: Account Connection Test
#!/bin/bash
ACCOUNT="user@example.com"
TOKEN="your-api-token"
echo "=== Account Connection Test ==="
# 1. Get account info
echo "1. Account info:"
curl -s "https://emailengine.example.com/v1/account/$ACCOUNT" \
-H "Authorization: Bearer $TOKEN" | jq '{state, lastError, authFailureDisabledAt}'
# 2. Request a reconnect (answers {"reconnect": false} for an account that was switched off)
echo "2. Testing reconnection:"
curl -X PUT "https://emailengine.example.com/v1/account/$ACCOUNT/reconnect" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"reconnect": true}'
# 3. Wait and check
sleep 10
echo "3. New state:"
curl -s "https://emailengine.example.com/v1/account/$ACCOUNT" \
-H "Authorization: Bearer $TOKEN" | jq '.state'
Log Analysis Tips
EmailEngine writes one JSON object per line. level is numeric: 30 is info, 40 warning, 50 error, 60 fatal.
Useful Log Commands
# View logs in real-time
journalctl -u emailengine -f
# Last 100 lines
journalctl -u emailengine -n 100
# Errors only
journalctl -u emailengine | grep '"level":50'
# Specific time range
journalctl -u emailengine --since "1 hour ago"
# Export logs
journalctl -u emailengine --since "today" > emailengine-$(date +%Y%m%d).log
# Entries for one account
journalctl -u emailengine | grep '"account":"user@example.com"'
# Count errors
journalctl -u emailengine --since "1 hour ago" | grep -c '"level":50'
Log Patterns to Look For
Connection issues (the code of the attached error):
grep -i "ECONNREFUSED\|ETIMEDOUT\|ENOTFOUND\|ECONNRESET" emailengine.log
Authentication failures (the IMAP client logs one line per rejected login, and the attached error carries the flag):
grep '"msg":"Failed to authenticate"\|"authenticationFailed":true' emailengine.log
Webhook failures:
grep '"msg":"Failed posting webhook"' emailengine.log
Getting Help
Information to Collect
When requesting support, provide:
-
EmailEngine version:
emailengine version -
System information:
uname -a
node --version
redis-server --version -
Configuration:
The environment variables or the TOML file the service runs with, for example
/etc/emailengine/config.tomlon a SystemD install. RemoveEENGINE_SECRET, passwords, and license keys before sharing. -
Logs:
journalctl -u emailengine -n 200 > logs.txt -
Account state:
curl https://emailengine.example.com/v1/accounts \
-H "Authorization: Bearer TOKEN" | \
jq '[.accounts[] | {account, state, lastError}]'
Support Channels
- Documentation: learn.emailengine.app
- GitHub Issues: github.com/postalsys/emailengine/issues
- Email support: support@emailengine.app, see Support
See Also
- Account troubleshooting - Connection, authentication, and sync failures
- Error codes - What a given code means and whether to retry
- Logging - Turning up detail on the server or one account
- Monitoring - Health checks and metrics
- Support - What to include when you ask for help