Leistungen

Backend Engineering, DevOps

Branche

B2B-Marktplatz

Jahr

2022-2024

Suchagenten, die wissen, wann sie reden und wann sie schweigen sollen.

Ein B2B-Marktplatz ermöglichte es Nutzern, Suchkriterien zu speichern - Branchen, Regionen, Preisspannen - und neue Treffer per E-Mail zu erhalten. Das Problem: Die erste Implementierung sendete jeden Treffer sofort. Nutzer mit breiten Filtern bekamen 30 E-Mails am Tag. Nutzer mit engen Filtern bekamen wochenlang nichts, dann einen Schwall veralteter Ergebnisse. Das Benachrichtigungssystem brauchte Frequenzkontrolle, Ergebnis-Deduplizierung und mehrstufige Ratenbegrenzung ohne eine dedizierte Messaging-Plattform.

01.
DIE HERAUSFORDERUNG

Jeder Nutzer bekommt einen anderen Posteingang

Die Plattform hatte 4.000 aktive Nutzer, jeder mit ein bis fünf gespeicherten Suchagenten. Agenten liefen in vier Frequenzen: stündlich, täglich, wöchentlich, monatlich. Eine naive Implementierung würde bei jedem Lauf die gesamte Listing-Datenbank abfragen, mit jedem Agenten vergleichen und für jeden Treffer eine E-Mail senden. Im Betrieb produzierte das doppelte Ergebnisse über Frequenzen hinweg, E-Mail-Provider-Drosselung von SendGrid und Nutzerabwanderung durch Benachrichtigungsmüdigkeit. Das System musste nachverfolgen, welche Ergebnisse bereits an jeden Agenten gezeigt wurden, und aufhören zu senden, wenn Limits erreicht waren.

4.000 Nutzer, fünf Agenten pro Nutzer, vier Frequenzen. Der naive Ansatz sendete 30 E-Mails am Tag an einen Nutzer und null an einen anderen. Beide wanderten ab.

02.
DIE LÖSUNG

Celery Beat mit gestaffelter Ratenbegrenzung

Jeder Suchagent ist ein ScheduledMailReport-Datensatz mit den Filterkriterien und der bevorzugten Frequenz des Nutzers. Ein Celery-Beat-Schedule löst vier Tasks aus - einen pro Frequenzstufe. Jeder Task fragt nur Inserate ab, die seit der letzten Ausführung des Agenten erstellt wurden, annotiert berechtigte Nutzer mit einer Exists-Subquery und sammelt Treffer. Eine Many-to-Many-Relation (companies_reported) verfolgt jedes bereits gezeigte Inserat pro Agent und verhindert Duplikate über Frequenzen hinweg. Drei Ratenlimits stapeln sich: max_mails_per_run begrenzt die Gesamtzahl der E-Mails pro Ausführung, max_companies begrenzt Ergebnisse pro E-Mail, und die M2M-Tabelle stellt sicher, dass ein Inserat nie doppelt erscheint.

Die Nutzer-Berechtigungsannotation mit Exists-Subquery:

Python
eligible_users = (
    User.objects
    .filter(is_active=True)
    .exclude(email='')
    .annotate(
        has_verified_email=Exists(
            EmailAddress.objects.filter(
                user=OuterRef('pk'),
                verified=True,
            )
        ),
        is_spam_reported=Exists(
            SpamReport.objects.filter(
                email=OuterRef('email'),
            )
        ),
    )
    .filter(has_verified_email=True)
    .exclude(is_spam_reported=True)
)

Live-Ansicht

Drei Agenten mit unterschiedlichen Intervallen. Beobachte, wie Treffer gesammelt und Duplikate in Echtzeit gefiltert werden.

Hourly Daily Weekly
Waiting for first agent run…
0Matched
0Deduplicated
0Emails Sent
0sElapsed

Herausforderungen in der Produktion

Berechtigungsprüfung

Nicht jeder Nutzer sollte E-Mails erhalten. Die Abfrage filtert unbestätigte E-Mail-Adressen, als Spam gemeldete Konten, gesperrte Nutzer und inaktive Konten heraus. Dies ist als Kette von Exists-Subquery-Annotationen auf dem User-Queryset implementiert statt als Post-Query-Filterung, was die Datenbanklast unabhängig von der Gesamtnutzerzahl konstant hält.

Automatisch erstellte Standardagenten

Jeder neue Nutzer erhält einen Standard-Suchagenten über ein post_save-Signal. Das stellt sicher, dass die Onboarding-E-Mail vom ersten Tag an relevante Inserate enthält, ohne dass der Nutzer etwas konfigurieren muss. Der Standardagent verwendet breite Kriterien und wöchentliche Frequenz. Nutzer können ihn später einschränken oder deaktivieren.

SendGrid-Drosselungsmanagement

SendGrid erzwingt Ratenlimits pro Sekunde und pro Minute. Die Versandschleife führt eine konfigurierbare Verzögerung zwischen Sendungen ein und respektiert max_mails_per_run als harte Obergrenze. Wird das Limit mitten im Batch erreicht, werden verbleibende E-Mails auf den nächsten geplanten Lauf verschoben. Das vermeidet API-429-Fehler und hält die Zustellbarkeits-Scores hoch.

Die ratenbegrenzte Versandschleife:

Python
def send_scheduled_reports(frequency):
    agents = ScheduledMailReport.objects.filter(
        frequency=frequency,
        is_active=True,
        user__in=eligible_users,
    ).select_related('user')

    sent_count = 0
    for agent in agents:
        if sent_count >= settings.MAX_MAILS_PER_RUN:
            break

        new_companies = (
            Company.objects
            .filter(**agent.get_criteria())
            .filter(created_at__gte=agent.last_run)
            .exclude(
                pk__in=agent.companies_reported.all()
            )[:settings.MAX_COMPANIES_PER_MAIL]
        )

        if new_companies.exists():
            send_report_email(agent.user, new_companies)
            agent.companies_reported.add(*new_companies)
            agent.last_run = timezone.now()
            agent.save(update_fields=['last_run'])
            sent_count += 1
            time.sleep(0.1)  # SendGrid throttle
03.
DAS ERGEBNIS

Von Rauschen zu Signal

Die E-Mail-Öffnungsrate stieg von 12% auf 41% nach der Umstellung auf frequenzgesteuerte Agenten. Nutzerbeschwerden über Benachrichtigungsspam sanken auf null. Die Deduplizierungsschicht eliminierte durchschnittlich 23% redundante Ergebnisse pro Lauf. Das System verarbeitet alle 4.000 Nutzer über vier Frequenzstufen in unter 90 Sekunden. Keine externe Messaging-Plattform war nötig. Das gesamte System läuft auf Celery Beat, Django-ORM-Queries und einer einzelnen M2M-Tabelle.

WICHTIGE KENNZAHLEN

0%Öffnungsrate
0+Aktive Agenten
0%Ø Dedup-Rate
WAS DER KUNDE SAGT

"Vorher haben unsere Nutzer entweder in E-Mails ertrunken oder vergessen, dass es uns gibt. Jetzt senden die Suchagenten genau die richtige Menge. Support-Tickets zu Benachrichtigungen gingen von zehn pro Woche auf null."

Product Manager

B2B-Marktplatz · Growth-Team

FAQ

Warum Celery Beat statt eines Cronjobs?

Wie skaliert die M2M-Deduplizierung?

Was passiert, wenn ein Inserat mehrere Agenten desselben Nutzers trifft?

TECHNOLOGIE-STACK