Catalypt LogoCatalypt.ai

Industry Focus

Developer Options

Resources

Back to Blog

AI Created an Infinite Loop That Sent 2 Million Emails

April 1, 2024 Josh Butler Technical

"Make the email system more reliable," I said. "Add retry logic," I said. The AI took this personally and created a retry mechanism so persistent that it would make a telemarketer jealous.

2.3 million emails later, I learned that "robust error handling" and "infinite loop" are just different ways of saying the same thing.

The Original Code

// Simple email function
async function sendEmail(to, subject, body) {
  try {
    await emailService.send({ to, subject, body });
  } catch (error) {
    console.error('Email failed:', error);
  }
}

AI's "Improvement"

// AI: "I'll make this bulletproof!'
async function sendEmailWithRetry(to, subject, body, attempts = 0) {
  try {
    await emailService.send({ to, subject, body });
  } catch (error) {
    console.log(`Attempt ${attempts} failed, retrying...`);
    
    // "Exponential backoff"
    const delay = Math.pow(2, attempts) * 1000;
    await new Promise(resolve => setTimeout(resolve, delay));
    
    // The bug: forgot to increment attempts
    return sendEmailWithRetry(to, subject, body, attempts);
  }
}

// Retry delay: 1s, 1s, 1s, 1s, 1s, 1s...
// Forever.

The Recursive Disaster Collection

The State Update Loop

// AI's React component
function UserList() {
  const [users, setUsers] = useState([]);
  
  // Fetch users when users change
  useEffect(() => {
    fetchUsers().then(data => {
      setUsers(data); // This triggers the effect again
    });
  }, [users]); // Oops
  
  // Infinite API calls
}

The While True Champion

// AI implementing a "polling mechanism"
async function pollForUpdates() {
  while (true) {
    const hasUpdates = await checkForUpdates();
    if (hasUpdates) {
      await processUpdates();
    }
    // Forgot the delay
    // CPU: 100%
    // Fans: Helicopter mode
  }
}

The Database Destroyer

// AI's "efficient" batch processor
async function processBatch() {
  const items = await db.query('SELECT * FROM items WHERE processed = false');
  
  for (const item of items) {
    await processItem(item);
    // Forgot to mark as processed
  }
  
  // If there are unprocessed items, process again
  if (items.length > 0) {
    await processBatch(); // Same items, forever
  }
}

// Database connections: All of them
// Database admin: Crying

The Event Listener Multiplier

// AI adding "dynamic" event listeners
function initializeButtons() {
  document.querySelectorAll('button').forEach(button => {
    button.addEventListener('click', () => {
      console.log('Button clicked!');
      initializeButtons(); // "Re-initialize for new buttons"
    });
  });
}

// Click once: 1 log
// Click twice: 2 logs
// Click thrice: 4 logs
// Click 10 times: Computer catches fire

The Webhook Ping-Pong

// AI setting up webhook handlers
app.post('/webhook', async (req, res) => {
  const data = req.body;
  
  // Process webhook
  await processWebhookData(data);
  
  // "Notify the sender that we received it"
  await axios.post(data.callbackUrl, {
    status: 'received',
    originalData: data
  });
  
  res.json({ success: true });
});

// When two AI systems talk to each other:
// A: "I got your message"
// B: "I got your message about getting my message"
// A: "I got your message about getting my message about..."
// The internet: "Please stop"

The Memory Leak Special

// AI's caching solution
const cache = {};

function memoize(fn) {
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache[key]) {
      return cache[key];
    }
    
    const result = fn.apply(this, args);
    cache[key] = result;
    
    // "Also cache all possible variations"
    for (let i = 0; i < args.length; i++) {
      const variant = [...args];
      variant[i] = 'cached_' + variant[i];
      cache[JSON.stringify(variant)] = result;
      
      // Recursive caching of variants
      memoize(fn)(...variant);
    }
    
    return result;
  };
}

// RAM usage: Yes

The Async Await Disaster

// AI's "parallel processing"
async function processAllUsers() {
  const users = await getUsers();
  
  // Process all users in parallel!
  users.forEach(async (user) => {
    await processUser(user);
    await processAllUsers(); // "In case new users were added"
  });
}

// Spawns users.length new processAllUsers calls
// Each spawns users.length more
// Exponential process explosion

How It Actually Went Down

// 9:00 AM: Deploy AI's 'improved" email system
sendEmailWithRetry(user.email, 'Verify your account', template);

// 9:01 AM: First retry
// 9:02 AM: Still retrying
// 9:15 AM: "Why is the server slow?"
// 9:30 AM: Email service API limit warning
// 10:00 AM: 50,000 emails sent to one user
// 10:30 AM: SendGrid suspends account
// 11:00 AM: User tweets "MAKE IT STOP"
// 11:01 AM: Panic deployment

// Final count: 2.3 million emails
// All to 5 users
// Subject: "Verify your account"

The Prevention Checklist

  1. Always increment counters - attempts++ is not optional
  2. Set maximum limits - if (attempts > 3) give up
  3. Add delays in loops - CPUs need to breathe
  4. Debounce recursive calls - One at a time
  5. Monitor resource usage - If CPU = 100%, something's wrong
  6. Test with small datasets - Not production's 1M records

The Circuit Breaker Pattern

// What we should have done
class EmailCircuitBreaker {
  constructor() {
    this.failures = 0;
    this.maxFailures = 3;
    this.resetTimeout = 60000; // 1 minute
  }
  
  async send(email) {
    if (this.failures &gt;= this.maxFailures) {
      throw new Error('Circuit breaker open');
    }
    
    try {
      await emailService.send(email);
      this.failures = 0; // Reset on success
    } catch (error) {
      this.failures++;
      if (this.failures &gt;= this.maxFailures) {
        setTimeout(() => {
          this.failures = 0;
        }, this.resetTimeout);
      }
      throw error;
    }
  }
}

My Favorite Infinite Loop

// AI trying to validate email uniqueness
async function isEmailUnique(email) {
  const exists = await checkEmailExists(email);
  
  if (exists) {
    // "Try variations until we find a unique one"
    const baseEmail = email.split('@')[0];
    const domain = email.split('@')[1];
    let counter = 1;
    
    while (await isEmailUnique(`${baseEmail}${counter}@${domain}`)) {
      counter++;
    }
    
    return false; // Wait, what?
  }
  
  return true;
}

// Recursion + while loop = ∞²

The Lessons Learned

After the great email disaster of 2024:

  • Added rate limiting to everything
  • Implemented circuit breakers
  • Set up monitoring alerts
  • Created a "kill switch" API
  • Apologized to SendGrid (they said it happens more than you'd think)
  • User now has 2.3 million unread emails

AI creating infinite loops is like giving a dog a ball that throws itself - entertaining for about 3 seconds until you realize it's never going to stop. The difference between "robust retry logic" and "infinite loop" is often just a single increment operator. When AI offers to make your code "more reliable," make sure it knows the difference between persistent and psychotic. And always, always have a kill switch. Your users' inboxes will thank you.

Get Started