Go: Worker-Pools vs. eine Goroutine pro Aufgabe für Hintergrundaufgaben
Go Worker-Pools vs. Goroutine-per-Task: Lerne, wie sich jedes Modell auf Durchsatz, Speicherverbrauch und Backpressure für Hintergrundverarbeitung und lang laufende Workflows auswirkt.

Welches Problem lösen wir?\n\nDie meisten Go-Services tun mehr, als nur HTTP-Anfragen zu beantworten. Sie führen auch Hintergrundarbeit aus: E‑Mails verschicken, Bilder skalieren, Rechnungen erzeugen, Daten synchronisieren, Events verarbeiten oder einen Suchindex neu aufbauen. Manche Jobs sind schnell und unabhängig. Andere bestehen aus langen Workflows, bei denen jeder Schritt vom vorherigen abhängt (Karte belasten, auf Bestätigung warten, Kunde benachrichtigen und Reports aktualisieren).\n\nWenn Leute "Go worker pools vs goroutine-per-task" vergleichen, versuchen sie meist ein Produktionsproblem zu lösen: Wie führt man viele Hintergrundaufgaben aus, ohne den Service langsam, teuer oder instabil zu machen?\n\nDie Auswirkungen spürt man an ein paar Stellen:\n\n- Latenz: Hintergrundarbeit entzieht CPU, Speicher, Datenbankverbindungen und Netzwerkbandbreite von user-facing Requests.\n- Kosten: Unkontrollierte Parallelität treibt dich zu größeren Maschinen, mehr DB-Kapazität oder höheren Queue-/API-Kosten.\n- Stabilität: Bursts (Imports, Marketing-Sends, Retry-Stürme) können Timeouts, OOM-Crashes oder Kaskadenfehler auslösen.\n\nDer eigentliche Trade-off ist Einfachheit vs. Kontrolle. Für jede Aufgabe eine Goroutine zu starten ist einfach zu schreiben und oft in Ordnung, wenn das Volumen gering oder natürlich begrenzt ist. Ein Worker-Pool bringt Struktur: feste Parallelität, klarere Limits und einen natürlichen Ort für Timeouts, Retries und Metriken. Der Preis ist etwas mehr Code und die Entscheidung, was passiert, wenn das System ausgelastet ist (warten Aufgaben, werden sie abgelehnt oder extern gespeichert?).\n\nEs geht hier um Alltags-Hintergrundverarbeitung: Durchsatz, Speicher und Backpressure (wie man Überlast vermeidet). Es wird nicht versucht, jede Queue-Technologie, verteilte Workflow-Engines oder genau-einmal-Semantik abzudecken.\n\nWenn du komplette Apps mit Hintergrundlogik auf einer Plattform wie AppMaster (appmaster.io) baust, tauchen dieselben Fragen schnell auf. Geschäftsprozesse und Integrationen brauchen weiterhin Limits gegenüber Datenbanken, externen APIs und E‑Mail/SMS-Anbietern, damit ein ausgelasteter Workflow nicht alles verlangsamt.\n\n## Zwei gängige Muster in einfachen Worten\n\n### Eine Goroutine pro Aufgabe\n\nDas ist der einfachste Ansatz: Wenn ein Job eintrifft, starte eine Goroutine, die ihn verarbeitet. Die „Queue“ ist oft das, was die Arbeit auslöst, z. B. ein Channel-Empfänger oder ein direkter Aufruf aus einem HTTP-Handler.\n\nTypisch ist: Job empfangen, dann go handle(job). Manchmal ist noch ein Channel im Spiel, aber eher als Übergabeort denn als Begrenzung.\n\nDas funktioniert gut, wenn Jobs hauptsächlich auf I/O warten (HTTP-Aufrufe, DB-Abfragen, Uploads), das Volumen moderat ist und Bursts klein oder vorhersehbar sind.\n\nDer Nachteil ist, dass die Parallelität ohne klares Limit wachsen kann. Das kann Speicher spiken, zu viele Verbindungen öffnen oder einen Downstream-Service überlasten.\n\n### Worker-Pool\n\nEin Worker-Pool startet eine feste Anzahl von Worker-Goroutinen und füttert sie mit Jobs aus einer Queue, meist einem in-memory gepufferten Channel. Jeder Worker läuft in einer Schleife: Job nehmen, verarbeiten, wiederholen.\n\nDer entscheidende Unterschied ist Kontrolle. Die Anzahl der Worker ist ein hartes Parallelitätslimit. Wenn Jobs schneller ankommen, als Worker sie abarbeiten, warten Jobs in der Queue (oder werden abgelehnt, wenn die Queue voll ist).\n\nWorker-Pools passen gut, wenn Arbeit CPU-intensiv ist (Bildverarbeitung, Report-Generierung), wenn man vorhersehbaren Ressourcenverbrauch braucht oder wenn man eine Datenbank oder Drittanbieter-API vor Bursts schützen muss.\n\n### Wo die Queue liegt\n\nBeide Muster können einen in-memory Channel nutzen, der schnell ist, aber beim Neustart verschwindet. Für „darf nicht verloren gehen“-Jobs oder lange Workflows wandert die Queue oft außerhalb des Prozesses (DB-Tabelle, Redis oder Message-Broker). In diesem Setup wählst du trotzdem zwischen Goroutine-per-Task und Worker-Pools; nur laufen sie als Konsumenten der externen Queue.\n\nEinfaches Beispiel: Braucht das System plötzlich 10.000 E‑Mails zu verschicken, kann Goroutine-per-Task versuchen, alle auf einmal abzusetzen. Ein Pool kann 50 gleichzeitig senden und den Rest kontrolliert warten lassen.\n\n## Durchsatz: was sich ändert und was nicht\n\nEs ist üblich, einen großen Durchsatzunterschied zwischen Worker-Pools und Goroutine-per-Task zu erwarten. Meistens wird der rohe Durchsatz aber von etwas anderem begrenzt, nicht davon, wie du Goroutinen startest.\n\nDer Durchsatz trifft häufig eine Decke am langsamsten gemeinsamen Ressourcenpunkt: Datenbank- oder API-Limits, Festplatte oder Netzwerkbandbreite, CPU-intensive Arbeit (JSON/PDF/Bildskalierung), Locks und geteilter Zustand oder Downstream-Services, die unter Last langsamer werden.\n\nWenn eine gemeinsame Ressource der Flaschenhals ist, machen mehr Goroutinen die Arbeit nicht schneller. Sie sorgen größtenteils für mehr Wartende an derselben Engstelle.\n\nGoroutine-per-Task kann gewinnen, wenn Tasks kurz sind, überwiegend I/O-bound und nicht um gemeinsame Limits konkurrieren. Goroutine-Startup ist günstig, und Go plant große Mengen gut. In einem „fetch, parse, write one row“-Loop kann das CPUs auslasten und Netzwerk-Latenz kaschieren.\n\nWorker-Pools sind vorteilhaft, wenn du teure Ressourcen begrenzen musst. Wenn jeder Job eine DB-Verbindung hält, Dateien öffnet, große Buffer alloziert oder eine API-Quote trifft, hält feste Parallelität den Service stabil und erreicht trotzdem maximal sicheren Durchsatz.\n\nLatenz (vor allem p99) zeigt oft den Unterschied. Goroutine-per-Task wirkt bei niedriger Last gut, bricht dann aber ein, wenn zu viele Tasks sich anhäufen. Pools führen zu Queueing-Verzögerung (Jobs warten auf einen freien Worker), das Verhalten ist aber gleichmäßiger, weil du eine Herde vermeidest, die um dasselbe Limit kämpft.\n\nEin einfaches Denkmodell:\n\n- Wenn Arbeit günstig und unabhängig ist, kann mehr Parallelität den Durchsatz erhöhen.\n- Wenn Arbeit von einem gemeinsamen Limit gesteuert wird, erhöht mehr Parallelität hauptsächlich die Wartezeit.\n- Wenn dir p99 wichtig ist, messe Queue-Zeit separat von Verarbeitungszeit.\n\n## Speicher- und Ressourcenverbrauch\n\nEin großer Teil der Diskussion Worker-Pool vs Goroutine-per-Task dreht sich eigentlich ums Memory. CPU lässt sich oft nach oben skalieren. Speicherausfälle sind plötzlicher und können den ganzen Service mitnehmen.\n\nEine Goroutine ist billig, aber nicht umsonst. Jede startet mit einem kleinen Stack, der wächst, wenn tiefer aufgerufen wird oder große lokale Variablen gehalten werden. Es gibt auch Scheduler- und Runtime-Overhead. Zehntausend Goroutinen sind oft kein Problem. Hunderttausend können überraschen, wenn jede Referenzen auf große Jobdaten hält.\n\nDer größere versteckte Kostenpunkt ist oft nicht die Goroutine selbst, sondern was sie am Leben hält. Wenn Tasks schneller ankommen, als sie fertig werden, erzeugt Goroutine-per-Task einen unbegrenzten Rückstau. Die „Queue“ kann implizit sein (Goroutinen, die auf Locks oder I/O warten) oder explizit (ein gepufferter Channel, ein Slice, ein in-memory Batch). So oder so wächst der Speicher mit dem Rückstau.\n\nWorker-Pools helfen, weil sie eine Begrenzung erzwingen. Mit festen Workern und einer begrenzten Queue erhältst du ein echtes Speicherkapazitätslimit und ein klares Fehlerverhalten: Sobald die Queue voll ist, blockierst du, schaust Last ab oder gibst das Problem an eine andere Stelle weiter.\n\nGrobe Abschätzung:\n\n- Peak-Goroutinen = Worker + in-flight Jobs + „wartende“ Jobs, die du erzeugt hast\n- Speicher pro Job = Payload (Bytes) + Metadaten + alles, worauf Referenziert wird (Requests, decodiertes JSON, DB-Zeilen)\n- Peak-Backlog-Speicher ~= wartende Jobs * Speicher pro Job\n\nBeispiel: Hält jeder Job eine 200 KB große Payload (oder referenziert einen 200 KB Objektgraphen) und du lässt 5.000 Jobs sich ansammeln, sind das ~1 GB nur für Payloads. Selbst wenn Goroutinen magisch frei wären, ist der Rückstau es nicht.\n\n## Backpressure: das System vor dem Kollabieren schützen\n\nBackpressure ist simpel: Wenn Arbeit schneller ankommt, als du sie abarbeiten kannst, drückt das System kontrolliert zurück, statt stillschweigend alles aufzustapeln. Ohne Backpressure wirst du nicht nur langsamer. Du bekommst Timeouts, steigenden Speicherverbrauch und Fehler, die schwer reproduzierbar sind.\n\nFehlende Backpressure bemerkst du oft, wenn ein Burst (Imports, E‑Mails, Exporte) Muster auslöst wie: Speicher steigt und fällt nicht mehr, Queue-Zeit wächst während die CPU beschäftigt bleibt, Latenzspitzen für unabhängige Requests, sich stapelnde Retries oder Fehler wie „too many open files“ und Connection-Pool-Exhaustion.\n\nEin praktisches Werkzeug ist ein begrenzter Channel: Begrenze, wie viele Jobs warten dürfen. Produzenten blockieren, wenn der Channel voll ist, was die Job-Erzeugung an der Quelle verlangsamt.\n\nBlocking ist nicht immer die richtige Wahl. Für optionale Arbeit wähle eine explizite Policy, damit Überlast vorhersehbar ist:\n\n- Wegwerfen von niedrigwertigen Tasks (z. B. doppelte Benachrichtigungen)\n- Batchen vieler kleiner Tasks zu einem Write- oder API-Call\n- Verzögern von Arbeit mit Jitter, um Retry-Spitzen zu vermeiden\n- Auslagern zu einer persistenten Queue und schnell zurückgeben\n- Last abwerfen mit klarem Fehler, wenn schon überlastet\n\nRate-Limiting und Timeouts sind ebenfalls Backpressure-Werkzeuge. Rate-Limits begrenzen, wie schnell du eine Abhängigkeit (E‑Mail-Provider, DB, Drittanbieter-API) treffen darfst. Timeouts begrenzen, wie lange ein Worker blockiert sein kann. Zusammen verhindern sie, dass eine langsame Abhängigkeit in einen kompletten Ausfall kippt.\n\nBeispiel: Monatsabschluss-Staement-Generierung. Wenn 10.000 Anfragen gleichzeitig kommen, können unbegrenzte Goroutinen 10.000 PDF-Renderings und Uploads auslösen. Mit begrenzter Queue und fixen Workern renderst und retryst du in sicherem Tempo.\n\n## Wie man Schritt für Schritt einen Worker-Pool baut\n\nEin Worker-Pool begrenzt Parallelität, indem er eine feste Anzahl Worker laufen lässt und Jobs aus einer Queue füttert.\n\n### 1) Wähle ein sicheres Parallelitätslimit\n\nBeginne mit dem, worauf deine Jobs ihre Zeit verwenden.\n\n- Für CPU-lastige Arbeit halte die Worker nahe an der Anzahl der CPU-Kerne.\n- Für I/O-lastige Arbeit (DB, HTTP, Storage) kannst du höher gehen, aber stoppe, wenn Abhängigkeiten anfangen zu timeouten oder zu drosseln.\n- Bei gemischter Arbeit messen und anpassen. Ein vernünftiger Startbereich ist oft 2x bis 10x der CPU-Kerne, dann feintunen.\n- Respektiere gemeinsame Limits. Wenn der DB-Pool 20 Verbindungen hat, kämpfen 200 Worker nur um diese 20.\n\n### 2) Wähle die Queue und ihre Größe\n\nEin gepufferter Channel ist beliebt, weil er eingebaut und leicht verständlich ist. Der Buffer ist dein Stoßdämpfer für Bursts.\n\nKleine Buffer zeigen Überlast schnell an (Sender blockieren früher). Große Buffer glätten Spitzen, können aber Probleme verstecken und Speicher sowie Latenz erhöhen. Dimensioniere den Buffer bewusst und entscheide, was beim Volllaufen passiert.\n\n### 3) Mach jede Aufgabe abbrechbar\n\nÜbergebe ein context.Context in jeden Job und sorge dafür, dass der Job-Code ihn benutzt (DB, HTTP). So stoppst du sauber bei Deploys, Shutdowns und Timeouts.\n\ngo\nfunc StartPool(ctx context.Context, workers, queueSize int, handle func(context.Context, Job) error) chan<- Job {\n jobs := make(chan Job, queueSize)\n for i := 0; i < workers; i++ {\n go func() {\n for {\n select {\n case <-ctx.Done():\n return\n case j := <-jobs:\n _ = handle(ctx, j)\n }\n }\n }()\n }\n return jobs\n}\n\n\n### 4) Füge die Metriken hinzu, die du wirklich brauchst\n\nWenn du nur ein paar Zahlen trackst, nimm diese:\n\n- Queue-Depth (wie weit hinter du bist)\n- Worker-Busy-Time (wie ausgelastet der Pool ist)\n- Task-Dauer (p50, p95, p99)\n- Fehlerquote (und Retry-Zahlen, falls du retries machst)\n\nDas reicht, um Worker-Anzahl und Queue-Größe anhand von Daten zu tunen, nicht nach Vermutungen.\n\n## Häufige Fehler und Fallen\n\nDie meisten Teams werden nicht durch das „falsche“ Muster verletzt. Sie werden durch kleine Defaults verletzt, die bei Traffic-Spitzen zu Ausfällen werden.\n\n### Wenn Goroutinen sich vervielfachen\n\nDie klassische Falle ist, unter einem Burst für jeden Job eine Goroutine zu spawnen. Ein paar hundert sind okay. Einige hunderttausend können Scheduler, Heap, Logs und Sockets überfluten. Selbst wenn jede Goroutine klein ist, summieren sich die Kosten, und die Erholung dauert, weil die Arbeit schon in-flight ist.\n\nEin anderer Fehler ist, einen riesigen gepufferten Channel als „Backpressure“ zu behandeln. Ein großer Buffer ist nur eine versteckte Queue. Er kann Zeit kaufen, aber auch Probleme verstecken, bis die Speichergrenze erreicht ist. Wenn du eine Queue brauchst, dimensioniere sie bewusst und entscheide, was bei Volllaufen passiert (blockieren, verwerfen, später erneut versuchen oder persistieren).\n\n### Versteckte Engpässe\n\nViele Hintergrundjobs sind nicht CPU-bound. Sie werden von Downstream-Limits begrenzt. Ignorierst du diese Limits, überrollt ein schneller Produzent einen langsamen Konsumenten.\n\nHäufige Fallen:\n\n- Keine Cancellation oder Timeout, sodass Worker ewig auf eine API oder DB-Abfrage blockieren können\n- Worker-Zahlen ohne Prüfung realer Limits wie DB-Verbindungen, Festplatten-I/O oder Drittanbieter-Rate-Limits\n- Retries, die die Last verstärken (sofortige Retries über 1.000 fehlgeschlagene Jobs)\n- Ein gemeinsamer Lock oder eine einzige Transaktion, die alles serialisiert, sodass „mehr Worker“ nur Overhead bringt\n- Fehlende Sichtbarkeit: keine Metriken für Queue-Tiefe, Job-Alter, Retry-Zahl und Worker-Auslastung\n\nBeispiel: Ein nächtlicher Export löst 20.000 „send notification“-Jobs aus. Trifft jeder Task die DB und einen E‑Mail-Provider, ist es leicht, Connection-Pools oder Quoten zu überschreiten. Ein Pool von 50 Workern mit Per-Task-Timeouts und einer kleinen Queue macht das Limit offensichtlich. Eine Goroutine pro Task plus riesiger Buffer macht das System so lange gesund, bis es plötzlich nicht mehr ist.\n\n## Beispiel: Burstige Exporte und Benachrichtigungen\n\nStell dir vor, ein Support-Team braucht Daten für ein Audit. Eine Person klickt "Export", dann machen ein paar Teammitglieder das gleiche, und plötzlich entstehen innerhalb einer Minute 5.000 Export-Jobs. Jeder Export liest aus der DB, formatiert ein CSV, speichert eine Datei und sendet eine Benachrichtigung (E‑Mail oder Telegram), wenn er fertig ist.\n\nMit Goroutine-per-Task fühlt sich das System kurz großartig an. Alle 5.000 Jobs starten fast sofort, und es sieht aus, als würde die Queue schnell leeren. Dann zeigen sich die Kosten: Tausende gleichzeitige DB-Abfragen kämpfen um Verbindungen, Speicher steigt, weil Jobs gleichzeitig Buffers halten, und Timeouts werden häufig. Jobs, die schnell fertig hätten sein können, stecken hinter Retries und langsamen Queries.\n\nMit einem Worker-Pool startet es langsamer, aber der Ablauf ist ruhiger. Bei 50 Workern arbeiten nur 50 Exporte schwer gleichzeitig. Die DB-Nutzung bleibt in vorhersehbarem Bereich, Buffers werden öfter wiederverwendet und die Latenz ist stabiler. Die Gesamtfertigstellungszeit lässt sich leichter schätzen: ungefähr (Jobs / Worker) * durchschnittliche Job-Dauer plus etwas Overhead.\n\nDer Unterschied ist nicht, dass Pools magisch schneller sind. Sie verhindern, dass das System sich selbst verletzt. Ein kontrollierter 50‑at‑a‑time-Lauf ist oft schneller als 5.000 Jobs, die gegeneinander kämpfen.\n\nWo du Backpressure anwendest, hängt davon ab, was du schützen willst:\n\n- Auf API-Ebene: lehne neue Export-Anfragen ab oder verzögere sie, wenn das System beschäftigt ist.\n- In der Queue: akzeptiere Anfragen, aber enqueue Jobs und drain sie in sicherer Geschwindigkeit.\n- Im Worker-Pool: cap die Parallelität für die teuren Teile (DB-Reads, File-Generierung, Notification-Send).\n- Pro Ressource: teile in separate Limits auf (z. B. 40 Worker für Exporte, aber nur 10 für Notifications).\n- Bei externen Calls: rate-limite E‑Mail/SMS/Telegram, damit du nicht geblockt wirst.\n\n## Kurze Checkliste vor dem Go-Live\n\nBevor du Hintergrundjobs in Produktion laufen lässt, geh Limits, Sichtbarkeit und Fehlerbehandlung durch. Die meisten Vorfälle werden nicht durch "langsamen Code" verursacht, sondern durch fehlende Schutzmechanismen bei Lastspitzen oder instabilen Abhängigkeiten.\n\n- Setze harte Max-Parallelitäten pro Abhängigkeit. Nimm nicht nur eine globale Zahl und hoffe, sie passt. Begrenze DB-Writes, ausgehende HTTP-Calls und CPU-intensive Arbeit separat.\n- Mach die Queue begrenzt und beobachtbar. Setze ein echtes Limit für ausstehende Jobs und exportiere ein paar Metriken: Queue-Tiefe, Alter des ältesten Jobs und Verarbeitungsrate.\n- Füge Retries mit Jitter und einen Dead-Letter-Pfad hinzu. Retry selektiv, streue Retries und verschiebe nach N Fehlern den Job in eine Dead-Letter-Queue oder eine "failed"-Tabelle mit genug Details zum Review und Replay.\n- Überprüfe Shutdown-Verhalten: drainen, abbrechen, sicher wieder aufnehmen. Entscheide, was bei Deploy oder Crash passieren soll. Mach Jobs idempotent, damit Reprocessing sicher ist, und speichere Fortschritt für lange Workflows.\n- Schütze das System mit Timeouts und Circuit-Breakern. Jeder externe Call braucht ein Timeout. Wenn eine Abhängigkeit down ist, fail fast (oder pausiere Intake), statt Arbeit aufzuschichten.\n\n## Praktische nächste Schritte\n\nWähle das Muster, das zu deinem normalen Tagesbild passt, nicht zum Idealzustand. Wenn Arbeit in Bursts ankommt (Uploads, Exporte, E‑Mail-Blasts), ist ein fester Worker-Pool mit begrenzter Queue meistens die sichere Standardwahl. Wenn Arbeit konstant und jedes Task klein ist, kann Goroutine-per-Task in Ordnung sein, solange du irgendwo Limits durchsetzt.\n\nDie gewinnende Wahl macht Fehler langweilig. Pools machen Limits offensichtlich. Goroutine-per-Task macht es einfach, Limits zu vergessen — bis der erste echte Spike kommt.\n\n### Einfach anfangen, dann Grenzen und Sichtbarkeit ergänzen\n\nStarte mit etwas Einfachem, aber füge zwei Kontrollen früh hinzu: eine Begrenzung der Parallelität und eine Möglichkeit, Queueing und Fehler zu sehen.\n\nEin praktischer Rollout-Plan:\n\n- Definiere die Arbeitslastform: bursty, steady oder gemischt (und was "Peak" bedeutet).\n- Setze ein hartes Limit für in-flight Arbeit (Poolgröße, Semaphore oder begrenzter Channel).\n- Entscheide, was passiert, wenn das Limit erreicht ist: blockieren, verwerfen oder einen klaren Fehler zurückgeben.\n- Füge Basis-Metriken hinzu: Queue-Depth, Time-in-Queue, Processing-Time, Retries und Dead-Letters.\n- Loadteste mit einem Burst, der 5x deines erwarteten Peaks ist, und beobachte Speicher und Latenz.\n\n### Wenn ein Pool nicht ausreicht\n\nWenn Workflows Minuten bis Tage laufen können, hat ein einfacher Pool oft Probleme, weil Arbeit nicht nur "einmal ausführen" ist. Du brauchst Zustand, Retries und Resume-Fähigkeit. Das bedeutet meist, Fortschritt zu persistieren, idempotente Schritte zu verwenden und Backoff anzuwenden. Es kann auch heißen, einen großen Job in kleinere Schritte zu zerlegen, damit du nach einem Crash sicher wieder aufnehmen kannst.\n\nWenn du ein komplettes Backend mit Workflows schneller ausliefern willst, kann AppMaster (appmaster.io) eine praktische Option sein: Du modellierst Daten und Geschäftslogik visuell, und es generiert echten Go-Code für das Backend, sodass du dieselbe Disziplin um Parallelitätslimits, Queueing und Backpressure einhalten kannst, ohne alles manuell zu verdrahten.
FAQ
Default to a worker pool when jobs can arrive in bursts or touch shared limits like DB connections, CPU, or external API quotas. Use goroutine-per-task when volume is modest, tasks are short, and you still have a clear limit somewhere (like a semaphore or rate limiter).
Starting a goroutine per task is fast to write and can have great throughput at low load, but it can create an unbounded backlog under spikes. A worker pool adds a hard concurrency cap and a clear place to apply timeouts, retries, and metrics, which usually makes production behavior more predictable.
Usually not much. In most systems, throughput is capped by a shared bottleneck such as the database, an external API, disk I/O, or CPU-heavy steps. More goroutines won’t beat that limit; they mostly increase waiting and contention.
Goroutine-per-task often has better latency at low load, then can get much worse at high load because everything competes at once. A pool can add queueing delay, but it tends to keep p99 steadier by preventing a thundering herd on the same dependencies.
The goroutine itself is usually not the biggest cost; the backlog is. If tasks pile up and each task holds onto job payloads or large objects, memory can climb quickly. A worker pool plus a bounded queue turns that into a defined memory ceiling and a predictable overload behavior.
Backpressure means you slow down or stop accepting new work when the system is already busy, instead of letting work pile up invisibly. A bounded queue is a simple form: when full, producers block or you return an error, which prevents runaway memory and connection exhaustion.
Start from the real limit. For CPU-heavy jobs, begin near the number of CPU cores. For I/O-heavy jobs, you can go higher, but stop increasing when your database, network, or third-party APIs start timing out or throttling, and make sure you respect connection pool sizes.
Pick a size that absorbs normal bursts but doesn’t hide trouble for minutes. Small buffers expose overload quickly; large buffers can increase memory usage and make users wait longer before failures show up. Decide upfront what happens when the queue is full: block, reject, drop, or persist elsewhere.
Use context.Context per job and ensure database and HTTP calls respect it. Set timeouts on external calls, and make shutdown behavior explicit so workers can stop cleanly without leaving hung goroutines or half-finished work.
Track queue depth, time spent waiting in the queue, task duration (p50/p95/p99), and error/retry counts. These metrics tell you whether you need more workers, a smaller queue, tighter timeouts, or stronger rate limiting against a dependency.


