Die Schleife wartet zwischen zwei Sendungen eine konfigurierbare Pause und behandelt max_mails_per_run als harte Obergrenze. Ist sie mitten im Batch erreicht, warten die restlichen Agenten auf den nächsten geplanten Lauf, statt sich in einer Queue zu stapeln. Keine 429-Antworten, und der Zustellbarkeits-Score bleibt, wo er hingehört.
Leistungen
Backend Engineering, DevOps
Branche
B2B-Marktplatz
Jahr
2022-2024
Suchagenten, die wissen, wann sie schweigen.
Ein Nutzer eines B2B-Marktplatzes hatte eine breite Suche gespeichert und bekam 30 E-Mails am Tag. Ein anderer hatte eine enge gespeichert, hörte wochenlang nichts und bekam dann einen Schwall veralteter Ergebnisse. Beide gingen. 4.000 Nutzer hatten Suchen wie diese gespeichert, und der Posteingang war der einzige Weg der Plattform, einen von ihnen zurückzuholen.
DIE HERAUSFORDERUNG
Jeder Nutzer bekommt einen anderen Posteingang
4.000 aktive Nutzer, ein bis fünf gespeicherte Agenten pro Person, vier Frequenzen: stündlich, täglich, wöchentlich, monatlich. Die erste Implementierung schickte jeden Treffer sofort raus. Unter Last hieß das: bei jedem Lauf die gesamte Inseratsdatenbank scannen, jedes Inserat gegen jeden Agenten prüfen und pro Treffer eine E-Mail. Ein Inserat, das zum täglichen und zum wöchentlichen Agenten eines Nutzers passte, ging zweimal raus. SendGrid begann, die Schübe zu drosseln. Nutzer mit breiten Filtern stellten den Absender stumm, Nutzer mit engen vergaßen, dass es die Plattform gab. Nichts hielt fest, was ein Agent schon gezeigt hatte, also konnte nichts verhindern, dass er es noch einmal zeigte.
DIE LÖSUNG
Celery Beat mit gestaffelter Ratenbegrenzung
Eine eigene Messaging-Plattform war der erste Vorschlag, verworfen, weil die Matching-Logik und jede Regel darüber, wer was bekommen darf, schon im Django-ORM lebten. Ein Cronjob war der zweite, verworfen, weil er einen eigenen Prozess, eine eigene Datenbankverbindung, Fehlerbehandlung und Logging gebraucht hätte. Stattdessen ist jeder Agent eine ScheduledMailReport-Zeile mit Kriterien und Frequenz des Nutzers. Celery Beat startet vier Tasks, einen pro Stufe. Ein Task lädt nur Inserate, die seit dem letzten Lauf des Agenten entstanden sind, grenzt die Empfänger mit Exists-Subqueries ein (bestätigte Adresse, nicht als Spam gemeldet, nicht gesperrt, aktiv) und sammelt Treffer. Drei Limits stapeln sich: max_mails_per_run begrenzt E-Mails pro Ausführung, max_companies begrenzt Ergebnisse pro E-Mail, und eine Many-to-Many-Tabelle, companies_reported, hält jedes (Agent, Inserat)-Paar fest, das rausgegangen ist. Der Preis ist diese Tabelle: Sie wächst mit jedem Versand und braucht einen Index und einen Aufräumjob. Dafür gibt sie eine Garantie, die kein Zeitstempel geben kann.
Die Versandschleife. Das exclude liest die Tabelle, das add schreibt sie nach dem Versand:
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 throttleDrei Agenten, ein Nutzer
Oben kommen Inserate mit einem Branchen-Tag an. Drei Agenten desselben Nutzers feuern in verdichteten Intervallen: stündlich nur Logistik, täglich Logistik und Maschinenbau, wöchentlich alles. Achte auf die Tabelle companies_reported: Schickt der tägliche Agent ein Maschinenbau-Inserat, wird das Paar aus derselben E-Mail auch für den wöchentlichen Agenten eingetragen, und der nächste Wochenlauf zählt es als bereits gemeldet.
Agentenläufe
Neue Inserate
- #1052Logistik
- #1051Maschinenbau
- #1050Chemie
- #1049Logistik
- #1048Maschinenbau
- #1047Logistik
- #1046Chemie
- StündlichLogistik9 Läufe
1 gefunden0 bereits gemeldet1 gesendet
- TäglichLogistik, Maschinenbau3 Läufe
3 gefunden2 bereits gemeldet1 gesendet
- Wöchentlichalle Branchen1 Läufe
10 gefunden7 bereits gemeldet3 gesendet
| Inserat | eingetragen für | |
|---|---|---|
| #1051 | TäglichWöchentlich | #9 |
| #1052 | StündlichTäglichWöchentlich | #8 |
| #1050 | Wöchentlich | #7 |
| #1046 | Wöchentlich | #7 |
| #1043 | Wöchentlich | #7 |
| #1049 | StündlichTäglichWöchentlich | #6 |
| #1048 | TäglichWöchentlich | #5 |
| #1047 | StündlichTäglichWöchentlich | #4 |
- Inserate erstellt
- 12
- E-Mails gesendet
- 9
- Ergebnisse zugestellt
- 12
- Duplikate vermieden
- 12
- Paare eingetragen
- 26
DAS ERGEBNIS
Von Rauschen zu Signal
Die Nutzer öffneten die E-Mails wieder: Die Öffnungsrate hat sich nach der Umstellung mehr als verdreifacht, und die Beschwerden über Benachrichtigungs-Spam hörten auf. Alle 4.000 Nutzer über vier Stufen sind in unter 90 Sekunden verarbeitet, auf Celery Beat, Django-ORM-Queries und einer einzigen M2M-Tabelle. Eine Messaging-Plattform wurde nicht gekauft, und das Produktteam ändert eine Frequenz im Schedule statt per Deployment.
WICHTIGE KENNZAHLEN
41%E-Mail-Öffnungsrate nach der Umstellung, vorher 12 %
4.000+Nutzer mit aktiven Suchagenten
23%Als Duplikat verworfene Ergebnisse, Ø pro Lauf
FÜR DEIN PROJEKT
- Wann es passt
Dein Produkt mailt Leuten neue Einträge, die zu gespeicherten Kriterien passen: Inserate, Jobs, Alerts, Preissenkungen. Wenn ein Eintrag zu zwei Abos derselben Person passen kann, hast du dieses Problem bereits.
- Was du prüfen solltest
Schau nach, was dein System nach einem Versand festhält. Wenn die Antwort ein Zeitstempel und sonst nichts ist, kannst du nicht belegen, dass ein Nutzer einen Eintrag genau einmal gesehen hat.
- Was es braucht
Ein Scheduler, den du ohnehin betreibst (hier Celery Beat), eine Join-Tabelle mit zusammengesetztem Index und ein monatlicher Aufräumjob. Keine Messaging-Plattform, kein zusätzlicher Dienst.
KUNDENSTIMME
Früher sind unsere Nutzer entweder in E-Mails ertrunken oder haben vergessen, dass es uns gibt. Jetzt schickt ein Agent, wenn er etwas Neues hat, und schweigt sonst. Support-Tickets zu Benachrichtigungen gingen von zehn pro Woche auf null.
Product Manager
B2B-Marktplatz
FAQ
TECHNOLOGIE-STACK
Django
Python
PostgreSQL
Dietmar Eglhofer - Geschäftsführer, Solvster & VIARESDaniel hat bei der Gründung und Umsetzung der Unternehmen Solvster und VIARES einen entscheidenden Beitrag geleistet. Er war verantwortlich für den gesamten technischen Aufbau der Onlinepräsenz, inklusive der Serversetups, der Entwicklung und Optimierung der Website sowie der Sicherstellung der Funktionalität. Besonders beeindruckt hat mich, wie proaktiv und lösungsorientiert er immer an Verbesserungen gearbeitet hat. Daniel war stets sehr responsiv und zuverlässig - es war eine Freude, mit ihm zusammenzuarbeiten. Seine Expertise und sein Engagement haben maßgeblich zum Erfolg beider Projekte beigetragen!
Schließe dich Dietmar an und starte dein nächstes Projekt mit Zuversicht.
Frei für neue ProjekteSchreibe mir