Overview
When you update multiple search params in quick succession, nuqs automatically batches them into a single URL update. This prevents redundant history entries and avoids browser rate limiting.
How batching works
Event loop batching
All state updates in the same JavaScript event loop tick are automatically batched:
From the README:
“You can call as many state update functions as needed in a single event loop tick, and they will be applied to the URL asynchronously.”
The update queue
Internally, nuqs uses a ThrottledQueue to manage updates:
When you call setState:
- Update is added to the queue
- If another update for the same key exists, it’s replaced
- Options are merged (preserving the most permissive settings)
- A flush is scheduled for the next tick
From packages/nuqs/src/lib/queues/throttle.ts:45-72:
Flushing the queue
The queue flushes on the next event loop tick, respecting the throttle limit:
Throttling
Default throttle
To prevent browser rate limiting, nuqs throttles URL updates:
- Chrome/Firefox: 50ms between updates (default)
- Safari 17+: 120ms between updates
- Safari <17: 320ms between updates
The default is automatically detected:
Why throttle?
Browsers rate-limit history API calls to prevent abuse:
- Safari allows only 100 updates per 10 seconds (Safari 17+) or 30 seconds (older)
- Exceeding this causes errors and breaks navigation
Throttling only affects URL updates and server requests. State updates are always immediate to keep UI responsive.
Custom throttle values
You can override the default throttle:
Minimum throttle value
Values below 50ms are ignored:
This prevents rate-limiting errors even if you try to disable throttling.
Debouncing
For high-frequency updates (like text input), debouncing waits for a pause before updating the URL:
Behavior:
- User types “h” → state updates, debounce starts
- User types “e” → state updates, debounce resets
- User types “llo” → state updates, debounce resets
- 300ms passes with no typing → URL updates to
?q=hello
This is especially useful with shallow: false to avoid server requests on every keystroke.
Await URL updates
All setState functions return a Promise that resolves when the URL has been updated:
From the README:
“If you wish to know when the URL has been updated, and what it contains, you can await the Promise returned by the state updater function, which gives you the updated URLSearchParameters object.”
Promise caching
The returned Promise is cached until the next flush:
From the README:
“The returned Promise is cached until the next flush to the URL occurs, so all calls to a setState (of any hook) in the same event loop tick will return the same Promise reference.”
Option merging
When batching updates with different options, nuqs merges them intelligently:
History mode
If any update uses push, all updates use push:
From packages/nuqs/src/lib/queues/throttle.ts:56-58:
If any update requests scroll, the page scrolls:
Shallow mode
If any update uses shallow: false, the server is notified:
Throttle time
The highest throttle value wins:
From packages/nuqs/src/lib/queues/throttle.ts:68-71:
Using useQueryStates
For related query params that should always update together, use useQueryStates:
From the README.
State updates: O(1)
State updates are synchronous and immediate:
URL updates: throttled
URL updates are async and throttled:
Memory usage
The queue stores only the latest value for each key:
Debugging batching
Enable debug logs to see batching in action:
You’ll see logs like:
From the README debugging section.
Edge cases
Aborting pending updates
Navigating away or unmounting aborts pending updates:
Infinite throttle
Setting throttleMs: Infinity prevents URL updates entirely:
From packages/nuqs/src/lib/queues/throttle.ts:93-96:
Concurrent rendering
Adapters control whether to reset the queue immediately or wait:
Next.js sets this to false to handle React 18’s concurrent rendering:
This prevents race conditions during Suspense boundaries and concurrent updates.