Deliverability14 min read

Feedback Loops & Complaint Monitoring: The Invisible Deliverability Killer You're Not Tracking

Stop flying blind on spam complaints. Set up feedback loops (FBL) with Yahoo, Microsoft, and Gmail to track who's marking your emails as spam and suppress them before your reputation tanks.

R

React Emails Pro

March 2, 2026

Your password reset emails are going to spam. Users can't log in. Support tickets are piling up. You check your dashboard—deliverability looks fine. SPF, DKIM, DMARC: all green. Bounce rate: 0.8%. So what's the problem?

Complaint rate. Users are marking your emails as spam, and inbox providers are listening. But unlike bounces, most ESPs don't show you complaint data by default. You need feedback loops (FBL) to see it.

Industry benchmark: keep complaint rates below 0.1% (1 complaint per 1,000 emails). Above 0.3% and Gmail, Outlook, and Yahoo start routing your mail to spam for everyone, not just complainers.

What is a feedback loop (FBL)?

A feedback loop is a notification service offered by major email providers. When a user marks your email as spam, the provider sends a report back to you (if you're registered).

FBLs let you see who is complaining and which emails triggered the complaint. This is critical intel for protecting sender reputation.

Why FBLs matter

  • Complaint data is mostly invisible. Without FBL, you're flying blind. Gmail doesn't tell you when users hit spam.
  • Complaints hurt more than bounces. A spam report signals "this sender is unwanted." Providers weight this heavily.
  • Proactive suppression prevents blocklists. Remove complainers immediately, before you hit the 0.3% threshold.
Think of FBL as your early warning system. By the time complaint rates show up in aggregate deliverability reports, your reputation is already damaged.

Which providers offer FBL?

Not all inbox providers support feedback loops. Here's the current landscape:

Providers with FBL

  • Yahoo / AOL / Verizon Media. Comprehensive FBL. Easy registration via complaint-feedback-loop@cc.yahoo-inc.com
  • Microsoft (Outlook.com, Hotmail, Live). JMRP and SNDS programs. Registration required.
  • Fastmail. FBL available upon request
  • Some ISPs. Comcast, Cox, Charter (mostly for bulk senders)

Providers without FBL

  • Gmail. No public FBL. Google Postmaster Tools provides aggregate complaint data but no individual reports.
  • Apple Mail (iCloud). No FBL. No public complaint data.
  • ProtonMail. No FBL
For Gmail, use Google Postmaster Tools to track aggregate spam rates. It won't tell you who complained, but it'll show trends and warn you before blocklist.

How to register for FBL (step-by-step)

FBL registration varies by provider. Here's how to set up the big ones.

Yahoo / AOL FBL

Yahoo offers one of the most comprehensive FBLs. Registration is straightforward:

  1. Email Yahoo. Send a request to complaint-feedback-loop@cc.yahoo-inc.com
  2. Include these details:
    • Your sending domain (e.g., mail.yourapp.com)
    • Your sending IP address(es)
    • FBL destination email address (where reports should go)
    • Brief description of your sending program
  3. Wait for approval. Usually 3-5 business days
  4. Start receiving reports. Yahoo will email complaint reports in ARF (Abuse Reporting Format)
yahoo-fbl-request.txt
To: complaint-feedback-loop@cc.yahoo-inc.com
Subject: FBL Registration Request for yourapp.com

Hello,

I would like to register for Yahoo's Feedback Loop for our transactional email sending.

Domain: mail.yourapp.com
Sending IPs: 192.0.2.10, 192.0.2.11
FBL Reports Email: fbl@yourapp.com
Description: We send transactional emails (password resets, receipts, notifications) to our SaaS users.

Thank you,
[Your Name]
[Your Email]

Microsoft (Outlook.com / Hotmail) FBL

Microsoft uses two programs: JMRP (Junk Mail Reporting Program) and SNDS (Smart Network Data Services).

  1. Create a Microsoft account. Sign up at postmaster.live.com
  2. Register your domain. Add your sending domain
  3. Enroll in JMRP. Navigate to JMRP section and provide FBL destination email
  4. Verify domain ownership. Add a TXT record to your DNS
  5. Start receiving reports. Microsoft sends ARF reports when users mark emails as junk
Microsoft JMRP reports include the full email that was marked as spam (if user consents). This is incredibly valuable for debugging which emails trigger complaints.

Google Postmaster Tools

Gmail doesn't offer individual FBL reports, but you can track aggregate complaint rates:

  1. Sign up. postmaster.google.com
  2. Add your domain. Enter your sending domain (e.g., mail.yourapp.com)
  3. Verify with DNS. Add the provided TXT record
  4. Monitor dashboard. Track spam rate, domain reputation, and delivery errors

Google Postmaster Tools shows:

  • Spam rate. Percentage of emails marked as spam (by authenticated sending domain)
  • Domain reputation. High, Medium, Low, or Bad
  • IP reputation. Reputation score for your sending IPs
  • Delivery errors. Rate limit errors, SPF/DKIM failures
Google Postmaster Tools data only appears after you reach a minimum sending volume (exact threshold undisclosed). New senders won't see data immediately.

Processing FBL reports (ARF format)

FBL reports arrive in ARF format (Abuse Reporting Format, RFC 5965). It's a structured email with three MIME parts:

  1. Human-readable explanation. Text description of the complaint
  2. Machine-readable report. Structured data (feedback-type, user-agent, etc.)
  3. Original message. Copy of the email that was marked as spam (headers + optionally body)
sample-arf-report.txt
From: staff@hotmail.com
To: fbl@yourapp.com
Subject: FW: Complaint about message from yourapp.com
Content-Type: multipart/report; report-type=feedback-report;
  boundary="----=_Part_12345"

------=_Part_12345
Content-Type: text/plain; charset="us-ascii"

This is an email abuse report for an email message received from IP
192.0.2.10 on Mon, 03 Mar 2026 14:35:28 +0000.

------=_Part_12345
Content-Type: message/feedback-report

Feedback-Type: abuse
User-Agent: Hotmail FBL/1.0
Version: 1.0
Original-Mail-From: noreply@mail.yourapp.com
Arrival-Date: Mon, 03 Mar 2026 14:35:28 +0000
Reported-Domain: mail.yourapp.com
Source-IP: 192.0.2.10

------=_Part_12345
Content-Type: message/rfc822

From: noreply@mail.yourapp.com
To: user@hotmail.com
Subject: Your password reset link
Date: Mon, 03 Mar 2026 14:30:00 +0000
Message-ID: <abc123@mail.yourapp.com>

[... original email headers and body ...]

------=_Part_12345--

The key data points:

  • Feedback-Type: abuse, fraud, virus, or other
  • Original-Mail-From: Sender address
  • Arrival-Date: When provider received the email
  • Source-IP: Your sending IP
  • Message-ID: From the original email (critical for matching to your send logs)
The Message-ID header is your correlation key. Use it to look up which user received the email, what template was used, and when it was sent.

Parsing ARF reports (code example)

Here's a Node.js example using mailparser to extract complaint data from ARF reports:

lib/fbl/arf-parser.ts
import { simpleParser } from "mailparser";

export interface FBLReport {
  feedbackType: string;
  userAgent: string;
  originalMailFrom: string;
  arrivalDate: string;
  sourceIP: string;
  messageId: string | null;
  recipientEmail: string | null;
}

export async function parseARFReport(rawEmail: string): Promise<FBLReport | null> {
  try {
    const parsed = await simpleParser(rawEmail);

    // ARF reports are multipart/report
    if (!parsed.headers.get("content-type")?.includes("multipart/report")) {
      console.log("[FBL] Not an ARF report");
      return null;
    }

    // Extract machine-readable part (message/feedback-report)
    let feedbackPart: string | null = null;
    let originalMessageId: string | null = null;
    let recipientEmail: string | null = null;

    // Parse MIME parts
    if (parsed.attachments) {
      for (const attachment of parsed.attachments) {
        if (attachment.contentType === "message/feedback-report") {
          feedbackPart = attachment.content.toString();
        }

        // Original message part
        if (attachment.contentType === "message/rfc822") {
          const originalMsg = await simpleParser(attachment.content);
          originalMessageId = originalMsg.messageId || null;
          recipientEmail = originalMsg.to?.text || null;
        }
      }
    }

    if (!feedbackPart) {
      console.log("[FBL] No feedback-report part found");
      return null;
    }

    // Parse feedback report fields
    const report: FBLReport = {
      feedbackType: extractField(feedbackPart, "Feedback-Type") || "unknown",
      userAgent: extractField(feedbackPart, "User-Agent") || "unknown",
      originalMailFrom: extractField(feedbackPart, "Original-Mail-From") || "",
      arrivalDate: extractField(feedbackPart, "Arrival-Date") || "",
      sourceIP: extractField(feedbackPart, "Source-IP") || "",
      messageId: originalMessageId,
      recipientEmail,
    };

    return report;
  } catch (error) {
    console.error("[FBL] Error parsing ARF report:", error);
    return null;
  }
}

function extractField(text: string, fieldName: string): string | null {
  const regex = new RegExp(`^${fieldName}:\\s*(.+)$`, "mi");
  const match = text.match(regex);
  return match ? match[1].trim() : null;
}

Automated suppression

Once you parse the complaint, suppress the recipient immediately. Never send to them again.

lib/fbl/complaint-handler.ts
import { prisma } from "@/lib/db";
import { parseARFReport } from "./arf-parser";

export async function handleComplaintReport(rawEmail: string) {
  const report = await parseARFReport(rawEmail);

  if (!report || !report.recipientEmail) {
    console.log("[FBL] Unable to extract recipient from complaint");
    return;
  }

  const email = report.recipientEmail;

  // Suppress user permanently
  await prisma.user.update({
    where: { email },
    data: {
      emailSuppressed: true,
      suppressedAt: new Date(),
      suppressionReason: "spam_complaint",
    },
  });

  // Log the complaint for analysis
  await prisma.complaintLog.create({
    data: {
      email,
      feedbackType: report.feedbackType,
      userAgent: report.userAgent,
      messageId: report.messageId || "",
      sourceIP: report.sourceIP,
      arrivalDate: new Date(report.arrivalDate),
      rawReport: rawEmail,
    },
  });

  console.log(`[FBL] Suppressed ${email} due to spam complaint`);
}
Never email complainers again—even if they re-subscribe. Continued complaints after FBL reports can get you blocklisted by the provider.

Complaint rate monitoring

Track complaint rate as a core KPI. Here's a query to calculate it:

lib/analytics/complaint-rate.ts
import { prisma } from "@/lib/db";

export async function calculateComplaintRate(days: number = 7) {
  const since = new Date();
  since.setDate(since.getDate() - days);

  // Total emails sent
  const totalSent = await prisma.emailLog.count({
    where: {
      sentAt: { gte: since },
      status: "delivered",
    },
  });

  // Total complaints received
  const totalComplaints = await prisma.complaintLog.count({
    where: {
      createdAt: { gte: since },
    },
  });

  const complaintRate = totalSent > 0 ? (totalComplaints / totalSent) * 100 : 0;

  return {
    totalSent,
    totalComplaints,
    complaintRate: complaintRate.toFixed(4),
    period: `Last ${days} days`,
    status: complaintRate < 0.1 ? "healthy" : complaintRate < 0.3 ? "warning" : "critical",
  };
}

// Example usage
const stats = await calculateComplaintRate(7);
console.log(`Complaint rate: ${stats.complaintRate}%`);
console.log(`Status: ${stats.status}`);

// Alert if threshold exceeded
if (stats.complaintRate >= 0.3) {
  // Send alert to ops team
  await sendSlackAlert({
    channel: "#ops-alerts",
    message: `⚠️ CRITICAL: Complaint rate is ${stats.complaintRate}% (threshold: 0.3%).`,
  });
}

Segment analysis

Don't just track overall complaint rate. Segment by:

  • Email template. Which emails get the most complaints? (Password resets? Marketing?)
  • User cohort. New users vs long-term users? Free vs paid?
  • Sending time. Do late-night sends trigger more complaints?
  • Subject line. Are certain subjects flagged as spam more often?
lib/analytics/complaint-analysis.ts
import { prisma } from "@/lib/db";

export async function analyzeComplaintsByTemplate(days: number = 30) {
  const since = new Date();
  since.setDate(since.getDate() - days);

  const complaints = await prisma.complaintLog.findMany({
    where: { createdAt: { gte: since } },
    include: {
      emailLog: {
        select: {
          templateName: true,
          subject: true,
        },
      },
    },
  });

  // Group by template
  const byTemplate: Record<string, number> = {};

  for (const complaint of complaints) {
    const template = complaint.emailLog?.templateName || "unknown";
    byTemplate[template] = (byTemplate[template] || 0) + 1;
  }

  // Sort by complaint count
  const sorted = Object.entries(byTemplate)
    .sort(([, a], [, b]) => b - a)
    .map(([template, count]) => ({ template, count }));

  return sorted;
}

// Example output:
// [
//   { template: "password-reset", count: 12 },
//   { template: "marketing-newsletter", count: 45 },
//   { template: "welcome", count: 3 },
// ]
If one template has a disproportionately high complaint rate, investigate: Is the subject line misleading? Is the content unexpected? Is the unsubscribe link visible?

How to reduce complaint rate

Complaints happen when users feel your email is unwanted,unexpected, or unsafe. Here's how to fix each:

1. Make unsubscribe obvious

Hidden unsubscribe links drive spam reports. Users hit spam because it's easier than hunting for your unsubscribe link.

  • Add List-Unsubscribe header. Gmail and Outlook show a native unsubscribe button. See our List-Unsubscribe guide.
  • Footer unsubscribe. Clear link in every email footer (legally required for marketing).
  • One-click opt-out. Don't require login or confirmation pages. Just unsubscribe them.

2. Send expected emails only

Unexpected emails = spam reports. If users didn't explicitly opt in, don't send.

  • Double opt-in for marketing. Confirm subscriptions before adding to lists.
  • Clear value proposition. At signup, tell users exactly what emails they'll receive.
  • No "we noticed you visited" emails. Browsing your site doesn't grant consent.

3. Don't look like phishing

Security-critical emails (password resets, verifications) often get marked as spam because users assume they're phishing.

  • Consistent sender name. Always send from the same recognizable name (e.g., "YourApp Security").
  • Match your brand. Logo, colors, and voice should match your app.
  • Clear context. "You requested a password reset" (not "Click here now").

4. Respect engagement signals

If users never open your emails, stop sending. Low engagement = future spam folder placement.

  • Monitor open rates. Track per-user engagement.
  • Re-engagement campaigns. "Still want to hear from us?" before suppressing.
  • Suppress inactive users. No opens in 6 months? Stop sending.

5. Frequency caps

Sending 5 emails in one day? Users will hit spam to make it stop.

  • Daily send limits. Max 1-2 marketing emails per day.
  • User preferences. Let users choose frequency (daily vs weekly digest).
  • Batch transactional emails. Combine notifications into a single summary email.

FBL email pipeline (full workflow)

Here's how to wire FBL reports into your email system end-to-end:

  1. Register for FBL with Yahoo, Microsoft, etc.
  2. Create dedicated email. fbl@yourapp.com (or similar)
  3. Set up email forwarding. Forward FBL reports to a webhook or email parser
  4. Parse ARF reports. Extract recipient email and message ID
  5. Suppress complainers. Update database to never send again
  6. Log complaints. Track for analytics and debugging
  7. Alert on thresholds. Slack/email when complaint rate exceeds 0.1%
app/api/webhooks/fbl/route.ts
import { NextRequest, NextResponse } from "next/server";
import { handleComplaintReport } from "@/lib/fbl/complaint-handler";

export async function POST(req: NextRequest) {
  const rawEmail = await req.text();

  try {
    await handleComplaintReport(rawEmail);

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error("[FBL Webhook] Error processing complaint:", error);
    return NextResponse.json({ error: "Processing failed" }, { status: 500 });
  }
}
Use email parsing services like Mailgun, SendGrid, or Cloudflare Email Routing to forward FBL reports to a webhook. Don't manually check an inbox.

Production checklist

Before you launch transactional email, verify:

  • FBL registration with Yahoo, Microsoft
  • Google Postmaster Tools set up for aggregate spam tracking
  • ARF report parser (automated processing)
  • Automatic suppression on complaints
  • Complaint rate monitoring (alert if > 0.1%)
  • List-Unsubscribe header (reduce complaints)
  • Clear unsubscribe link in email footer
  • Engagement-based pruning (suppress inactive users)
Bottom line. Feedback loops turn invisible complaints into actionable data. Without FBL, you're guessing. With it, you can fix issues before they tank your reputation.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates