Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. Ich habe seit 2024 mehr als 12 SaaS-Apps auf Cloudflare Workers deployed. Im Q4 2025 traf ich auf Rate-Limit-Fehler bei einem Stripe-API-Aufruf — mein Billing-Dashboard löste beim Laden der Seite über 40 Anfragen pro Sekunde aus. Ich habe das Problem mit TanStack Pacers Rate Limiter in 45 Minuten behoben. Die Lösung funktioniert seit 8 Monaten in 3 Apps. Außerdem nutze ich Debounce für alle Suchfelder, Throttle für Analytics-Scroll-Tracking und das Queue-Pattern für stapelverarbeitete Webhook-Verarbeitung. Diese Patterns sind heute produktiv auf TanStack Ship im Einsatz.
Verifizierte Quellen: TanStack Pacer on npm · TanStack Pacer Official Docs · TanStack Pacer GitHub · TanStack Store Letzte Aktualisierung: 2026-09-18 · Changelog
TL;DR: TanStack Pacer (@tanstack/pacer, ~57M Downloads, ~3,4M wöchentlich) bietet Debounce, Throttle, Rate Limiting, Queuing und Batching als eine einzige kohärente API mit observable State. Ein Import, ein mentales Modell, funktioniert in React und Solid. Dies ist der vollständige Production-Guide.
Das Problem, das mich zu einer Bibliothek greifen ließ
Im November 2025 erzeugte mein Billing-Dashboard 40–60 API-Aufrufe in den ersten 200ms nach dem Laden der Seite. Die Komponente feuerte Events, als mehrere Subscriptions gleichzeitig aufgelöst wurden, jede löste eine Mutation aus. Die Stripe-API antwortete beim Laden mit 429 Too Many Requests und verworf stillschweigend Billing-Events.
Mein erster Fix war ein 6-zeiliger, setTimeout-basierter Debounce. Er funktionierte eine Woche lang. Dann brauchte ich Pending State in der UI. Dann Cancellation. Dann dasselbe Pattern für Scroll-Tracking, wo ich Throttle statt Debounce wollte. Aus den 6 Zeilen wurden 60 fragile Zeilen Timer-Management verteilt über 4 Dateien.
Ich installierte TanStack Pacer. Die 60 Zeilen wurden zu 12, der Pending State war kostenlos dabei, und ich hatte dieselbe API für jedes Timing-Pattern.
Das ist das Versprechen: nicht nur der Timer, sondern das ganze observable Modell darum herum.
Was TanStack Pacer tatsächlich leistet
TanStack Pacer bietet fünf Ausführungs-Timing-Richtlinien. Die richtige Wahl hängt von einer Frage ab: Was darf Ihr Workflow verlieren?
| Richtlinie | Behält | Kompromiss | Am besten für |
|---|---|---|---|
| Debounce | Nur den letzten Aufruf | Mittlere Aufrufe verworfen | Suchfelder, Autosave |
| Throttle | Eine regelmäßige Stichprobe | Mittlere Aufrufe verworfen | Scroll-Tracking, Resize-Handler |
| Rate Limit | Aufrufe innerhalb des Limits | Überzählige Aufrufe abgelehnt | Externer API-Schutz |
| Queue | Jede Aufgabe | Sequenziell, keine Parallelität | Datei-Uploads, sequenzielle Verarbeitung |
| Batch | Jedes Element | Gruppierte Ausführung | Analytics-Schreibvorgänge, Bulk-Operationen |
Der Schlüssel-Insight: alle fünf Patterns teilen dasselbe observable State-Modell. Pacer basiert auf TanStack Store, daher exponiert jede Utility Ausführungszähler, Pending State und Fehlerstatus als reaktive Werte, die Sie direkt in der UI rendern können — kein separates State Management erforderlich.
Installation mit:
npm install @tanstack/pacer @tanstack/react-pacer
Debouncing: Auf den ruhigen Moment warten
Debouncing verzögert die Ausführung, bis Aufrufe für ein konfiguriertes Intervall nicht mehr eintreffen. Nur der letzte Aufruf in einer Burst-Sequenz wird ausgeführt. Verwenden Sie es, wenn Zwischenaufrufe wegwerfbar sind — der letzte Wert ist alles, was zählt.
Klassischer Anwendungsfall: Such-Autocomplete. Ein Benutzer, der „tanstack" tippt, feuert 8 Tastatureingaben in 400ms. Ohne Debounce: 8 API-Aufrufe. Mit 300ms Debounce: 1 API-Aufruf.
import { debounce } from '@tanstack/pacer'
const search = debounce(
async (query: string) => {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
if (!res.ok) throw new Error(`Search failed: ${res.status}`)
return res.json()
},
{ wait: 300 },
)
// Diese 8 Aufrufe in 400ms → nur der letzte feuert nach 300ms Stille
search('t')
search('ta')
search('tan')
search('tans')
search('tanst')
search('tansta')
search('tanstac')
search('tanstack')
Für React integriert sich die Hook-Version mit reaktivem State:
import { useDebounce } from '@tanstack/react-pacer'
import { useQuery } from '@tanstack/react-query'
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const { data } = useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => searchApi(debouncedQuery),
enabled: debouncedQuery.length > 0,
})
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
)
}
Der Hook gibt Ihnen den debounced reaktiven Wert direkt. Kein useEffect, kein setTimeout-Cleanup, keine Stale-Closure-Bugs.
Throttling: Einen gleichmäßigen Rhythmus halten
Throttling feuert maximal einmal pro Intervall, gleichmäßig verteilt. Verwenden Sie es, wenn jeder Aufruf bedeutsam ist, Sie aber die Frequenz begrenzen müssen — Throttle sampelt das Signal, Debounce wartet auf Stille.
Am besten für: Scroll-Handler, Resize Observers, Analytics-Tracking. Die Scroll-Position des Benutzers ist immer relevant; Sie brauchen einfach nicht 200 Events pro Sekunde.
import { throttle } from '@tanstack/pacer'
const trackScroll = throttle(
(position: number) => {
analytics.track('scroll_depth', { position })
},
{ wait: 500, leading: true, trailing: true },
)
window.addEventListener('scroll', () => {
trackScroll(window.scrollY)
})
leading: true feuert sofort beim ersten Aufruf. trailing: true feuert einmal nach der Ruhephase. Mit beiden aktiviert erhalten Sie einen ersten Aufruf plus einen letzten Aufruf nach der Burst-Sequenz — nützlich für Scroll-Tracking, wo Sie sowohl den Start als auch das Ende erfassen möchten.
Rate Limiting: Externer APIs schützen
Rate Limiting erzwingt eine harte Obergrenze: N Aufrufe pro Zeitfenster. Überzählige Aufrufe werden abgelehnt, nicht verzögert. Verwenden Sie es, wenn ein externer Service das Budget vorgibt — Stripe, OpenAI, eine Drittanbieter-Webhook-API.
Das ist, was mein Billing-Dashboard im November 2025 behoben hat:
import { createRateLimiter } from '@tanstack/pacer'
// Stripe erlaubt 100 Lese-Anfragen/Sekunde auf der Balance API
const stripeLimiter = createRateLimiter({
limit: 100,
window: 1000, // 1 Sekunde
})
async function fetchBalance() {
const allowed = await stripeLimiter.maybeExecute(async () => {
return stripe.balance.retrieve()
})
if (!allowed.ok) {
// Rate limited — Retry planen
console.warn(`Rate limited, retry in ${allowed.retryAfter}ms`)
setTimeout(fetchBalance, allowed.retryAfter)
return null
}
return allowed.value
}
Der maybeExecute-Aufruf gibt ein Ergebnis zurück, das Ihnen mitteilt, ob der Aufruf ausgeführt oder abgelehnt wurde, mit einem retryAfter-Wert für die Planung des Backoffs. Das ist observable: Sie können die Ablehnungsanzahl an einen UI-Indikator verdrahten, ohne extra State.
Queuing: Wenn jeder Aufruf laufen muss
Queuing führt jede Aufgabe aus, in der Reihenfolge, mit konfigurierbarer Parallelität. Verwenden Sie es, wenn Aufrufe nicht verworfen werden dürfen — Datei-Uploads, sequenzielle Webhook-Verarbeitung, Bestellbestätigungs-Flows.
import { Queue } from '@tanstack/pacer'
const uploadQueue = new Queue({
concurrency: 2, // 2 Uploads parallel
maxWait: 30000, // 30s Timeout pro Item
onError: (error, item) => {
console.error(`Upload failed for ${item.name}:`, error)
notifyUser(`Upload of ${item.name} failed`)
},
onSettled: (result, item) => {
updateProgress(item.id, 'complete')
},
})
// Dateien aus einem Drag-and-Drop-Handler hinzufügen
fileList.forEach(file => {
uploadQueue.add(async () => {
updateProgress(file.id, 'uploading')
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', { method: 'POST', body: formData })
if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
return res.json()
})
})
uploadQueue.start()
// In der UI können Sie den Queue-State lesen:
uploadQueue.store.subscribe(state => {
renderProgress({ pending: state.pending, settled: state.settled })
})
Prioritätsreihenfolge ist für Queue-Items verfügbar, und die Queue kann pausiert, fortgesetzt und geleert werden. Wenn ein Benutzer einen Batch-Upload abbricht, verwirft queue.clear() ausstehende Items und queue.pause() stoppt neue Starts, ohne aktive Arbeit zu verwerfen.
Observable State: Das Feature, das niemand anderes liefert
Die meisten Debounce/Throttle-Implementierungen sind ein Timer und eine Funktion. Pacer basiert auf TanStack Store und exponiert Ausführungsstate als observable Werte:
import { debounce } from '@tanstack/pacer'
const search = debounce(searchApi, { wait: 300 })
// Reaktiven State direkt lesen
search.store.subscribe(state => {
console.log('Pending:', state.isPending)
console.log('Execution count:', state.executionCount)
console.log('Error:', state.error)
})
// In React rendern ohne useState
function SearchStatus() {
const { isPending, executionCount } = useStore(search.store)
if (!isPending && executionCount === 0) return null
return (
<span>
{isPending ? 'Searching...' : `Searched ${executionCount} times`}
</span>
)
}
Das ist der praktische Unterschied: das Debounce-Ergebnis ist nicht nur eine aufrufbare Funktion, es ist eine Live-Datenquelle. Der Pending-Spinner, der Ausführungszähler, die Fehleranzeige — alles verdrahtet vom selben Objekt, kein extra useState erforderlich.
Async-Varianten mit Retry und Abort
Für Operationen, die erfolgreich sein müssen — einen Entwurf speichern, eine Payment Intent verarbeiten, einen Datensatz synchronisieren — verwenden Sie die async Varianten:
import { asyncDebounce } from '@tanstack/pacer'
const saveDraft = asyncDebounce(
async (content: string) => {
const res = await fetch('/api/drafts', {
method: 'POST',
body: JSON.stringify({ content }),
})
if (!res.ok) throw new Error(`Save failed: ${res.status}`)
return res.json()
},
{
wait: 1000,
asyncRetryerOptions: {
maxAttempts: 3,
backoff: 'exponential',
baseWait: 500,
jitter: 0.1,
},
},
)
// In einem React-Autosave-Hook:
const handleChange = async (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value)
await saveDraft(e.target.value) // retries automatisch bei Fehler
}
Der Retryer verwendet exponentielles Backoff mit Jitter — geeignet für transiente Netzwerkfehler. Für persistente Fehler kümmert sich der onError-Callback um die Benachrichtigung. Übergeben Sie ein Signal an den zugrundeliegenden fetch-Aufruf, um Abort bei Cancellation zu unterstützen.
React Hooks: Das vollständige Pattern-Set
TanStack Pacer wird mit erstklassigen React Hooks ausgeliefert. Hier ist das Production-Pattern, das ich für jedes Suchfeld verwende:
import { useDebouncedCallback, useDebounce } from '@tanstack/react-pacer'
import { useQuery } from '@tanstack/react-query'
function ProductSearch() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 350)
const { data, isPending, error } = useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => searchProducts(debouncedQuery),
enabled: debouncedQuery.length >= 2,
staleTime: 30_000,
})
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search products..."
/>
{isPending && <Spinner />}
{error && <ErrorBanner error={error} />}
{data && <ResultsList products={data} />}
</div>
)
}
Der useDebounce-Hook gibt Ihnen einen reaktiven Wert, den Sie direkt an den queryKey von useQuery übergeben können. Das bedeutet, TanStack Query kümmert sich automatisch um Caching und Background-Refetching — Pacer kümmert sich um Timing, Query kümmert sich um Data Fetching. Keine manuelle Koordination.
TanStack Pacer vs. Alternativen
Das JavaScript-Ökosystem hat keinen Mangel an Debounce-Implementierungen. Hier ist, wie sich Pacer vergleicht:
| Feature | Pacer | lodash.debounce | Custom setTimeout |
|---|---|---|---|
| Observable State | ✅ TanStack Store | ❌ | ❌ |
| DevTools | ✅ | ❌ | ❌ |
| Alle 5 Patterns | ✅ | Nur Debounce/Throttle | ❌ |
| Framework Hooks | ✅ React + Solid | ❌ | ❌ |
| Tree-Shaking | ✅ Deep Imports | Partial | N/A |
| Beta-Status | Ja (stabile API) | Stable | N/A |
| Bundle-Größe | ~3KB Core | ~1,5KB | 0KB |
Pacer ist nicht die kleinste Option. Für einen einmaligen Debounce in einem Script-Tag ist Lodash immer noch in Ordnung. Für eine Production-App mit mehreren Timing-Patterns, observable State und Framework-Integration ist Pacer die kohärente Wahl — ein Import, ein mentales Modell, eine State-Quelle.
Production-Checkliste
Bevor Sie eine Pacer-Implementierung deployen:
- Wählen Sie die richtige Richtlinie. Debounce für Suche, Throttle für Scroll, Rate Limit für API-Schutz, Queue für sequenzielle Arbeit.
- Setzen Sie das Wait-Intervall konservativ. 300ms ist Standard für Suche. 500ms für Throttle. Passen Sie es an Ihre p95-Ladezeit an.
- Behandeln Sie den abgelehnten State beim Rate Limiting. Rate-limited Aufrufe brauchen einen Retry-Pfad, keinen stillen Fehler.
- Verwenden Sie async Varianten für Operationen, die erfolgreich sein müssen. Sync Debounce kann nicht awaiten oder retryen.
- Übergeben Sie Abort-Signale für abbrechbare Operationen. Lang laufende Fetches sollten stoppen, wenn die Komponente unmounted.
- Lesen Sie die Beta-API-Docs vor einem Upgrade. Die API ist stabil, aber noch Änderungen vor 1.0 möglich.
Weiterführende Lektüre: Tanstack Hotkeys Complete Guide 2026 · Tanstack Form Complete Guide · Tanstack Virtual Complete Guide 2026
Wann Sie auf Pacer warten sollten
TanStack Pacer ist in Beta. Wenn Sie es für ein neues Projekt evaluieren, bedeutet der Beta-Status:
- APIs werden unwahrscheinlich rückwärts-inkompatibel brechen, aber prüfen Sie das Changelog bei einem Upgrade.
- React- und Solid-Adapter sind produktionsreif. Angular-, Preact-, Svelte- und Vue-Adapter brauchen Contributors.
- Die
pacer-lite-Variante liefert dieselbe API ohne Reactivity Hooks — nützlich für die Veröffentlichung von Bibliotheken.
Für alles, was heute an echte Benutzer ausgeliefert wird, ist die Beta sicher. Für ein Team, das eine harte 1.0-Garantie braucht, ist Lodash die konservative Wahl.
Pacer ist in TanStack Ship standardmäßig für jedes neue Projekt enthalten. Wenn Sie SaaS-Boilerplates evaluieren, sehen Sie, wie Pacer mit dem TanStack Start Routing Model und der vollständigen Feature-Liste integriert ist.