QA How-To
Testing bulk and batch endpoints (2026)
Learn testing bulk and batch endpoints for atomicity, partial success, validation, retries, ordering, authorization, performance, and observable results.
23 min read | 3,533 words
TL;DR
Testing bulk and batch endpoints starts by identifying the processing contract: all-or-nothing, per-item partial success, or asynchronous acceptance. Cover envelope and item validation, authorization, correlation, ordering, duplicates, retries, concurrency, limits, job lifecycle, and final database or event reconciliation.
Key Takeaways
- Clarify whether a request is atomic, best-effort, or asynchronous before defining expected results.
- Give every input item a stable correlation key so duplicates, reordering, retries, and partial failures remain testable.
- Test validation and authorization per item as well as at the request envelope.
- Verify committed data and downstream side effects, not only the aggregate HTTP response.
- Exercise duplicate items, overlapping concurrent batches, transport interruption, idempotency keys, and retry windows.
- Measure accepted payload limits, queue behavior, time budgets, and result retention without relying on arbitrary large files.
- Reconcile accepted, succeeded, failed, skipped, and pending counts against the original request.
Testing bulk and batch endpoints requires more than sending a large JSON array and checking for 200. One request can contain valid, invalid, unauthorized, duplicated, and conflicting items. Processing may be atomic, best-effort, queued, reordered, retried, or split across workers, so the HTTP response is often only one part of the oracle.
A reliable QA strategy begins with the processing contract. This guide shows how to distinguish bulk and batch behavior, build a compact input matrix, verify partial results and side effects, test idempotency and concurrency, and automate representative rules with runnable JavaScript.
TL;DR
| Contract | Initial response | Failure unit | Essential assertion |
|---|---|---|---|
| Atomic bulk | Final success or rejection | Entire request | No item commits if any required item fails |
| Best-effort bulk | Final mixed result | Individual item | Each result maps to exactly one input and counts reconcile |
| Asynchronous batch | Accepted job reference | Job or item later | Poll or receive completion, then reconcile durable outcomes |
| Streaming import | Progress and terminal summary | Row, chunk, or job | Check checkpoint, replay, row errors, and terminal counts |
Before execution, document item identity, maximum count and bytes, ordering rules, duplicate behavior, transaction boundary, authorization scope, timeout, retry policy, result retention, and cancellation semantics. Without these answers, a tester can observe behavior but cannot call it correct.
1. Distinguish Bulk Requests From Batch Processing
A bulk endpoint accepts multiple operations in one client request. A batch system schedules or executes work as a group, often asynchronously. Products frequently use the words interchangeably, so test the actual lifecycle instead of relying on the label. POST /users/bulk might process 100 users synchronously, while POST /imports may return a job ID and process a file later.
Ask when the client receives a durable commitment. A synchronous atomic API usually returns only after the transaction commits or rolls back. A best-effort API returns per-item outcomes after attempting each item. An asynchronous API may return 202 Accepted after validating only the envelope, with item validation deferred to workers. The 202 response does not mean that any business item succeeded.
Define the unit of work. It may be one array element, one operation object, one CSV row, one partition, or the entire job. Then define the unit of failure and retry. A worker may retry one item, a chunk, or the complete message. That choice affects duplicates and ordering.
Also identify the result channel. Outcomes may appear in the response body, a job-status endpoint, a downloadable error file, webhook, event stream, audit table, or notification. Each channel needs correlation and retention rules. If a job expires after a documented window, verify both availability within the window and safe behavior after expiry.
Create a lifecycle diagram in test notes: received -> validated -> accepted -> processing -> completed, partially completed, failed, or canceled. Verify permitted transitions and prevent impossible reversals such as a completed job returning to processing without a new attempt identity.
2. Define Atomicity and Partial Success Oracles
Atomic behavior means all required changes commit together or none commit. Best-effort behavior means independently valid items may commit while others fail. A hybrid may group items by partition or dependency. Testers must know which boundary the product promises.
| Scenario | Atomic expectation | Best-effort expectation |
|---|---|---|
| All items valid | Every item commits | Every item succeeds |
| One item invalid | No item commits | Valid items may succeed, invalid item fails |
| One item unauthorized | Entire request denied or rolled back per contract | Unauthorized item fails without blocking allowed items |
| Database failure after item 3 | Complete rollback | Committed items remain defined, later items fail or retry |
| Duplicate item in request | Whole request may reject | Duplicate gets an explicit result or documented coalescing |
For atomic cases, snapshot all touched records before the request, introduce one failure at the beginning, middle, and end, and compare after the response. Check generated IDs, counters, inventory, ledger entries, outbox events, and audit records. Rollback must include everything inside the documented transaction. An intentional audit of the rejected attempt can remain, but business side effects cannot leak.
For partial success, require one unambiguous result for every input item. A top-level failed: 2 without item keys is insufficient for safe recovery. Verify that accepted plus rejected plus pending equals submitted, accounting for documented de-duplication. Confirm that retryable and terminal errors are distinguishable.
Do not infer atomicity from a single run. An implementation may process sequentially and appear atomic when the invalid item happens to be first. Place a controlled failure at several positions and verify persistent state. If processing is concurrent, repeat with several orderings because completion position may differ from input position.
3. Build the Input Matrix for Testing Bulk and Batch Endpoints
Start with one valid item and expand dimensions deliberately. The envelope includes content type, authentication, idempotency key, batch metadata, array presence, item count, byte size, and schema version. Each item includes its own identifier, operation, fields, authorization context, dependencies, and validation rules.
Use these high-value partitions:
- Empty collection, one item, normal count, exact maximum, and maximum plus one.
- Small payload at count limit and large payload below count limit.
- All valid, all invalid, and one invalid among valid items.
- Invalid item first, middle, and last.
- Same item repeated identically and repeated with conflicting values.
- Existing and non-existing targets mixed together.
- Allowed and forbidden targets under the same authenticated subject.
- Independent items and items with an explicit dependency.
- Client correlation IDs that are missing, duplicated, long, or syntactically invalid.
Keep payloads minimal. A boundary test for 1,000 items does not need each object to contain a megabyte of filler. Test count and byte limits separately unless the contract combines them. Generate unique business identifiers so parallel runs do not collide, and save a manifest mapping input index, client key, target ID, and expected result.
Validate unknown fields and schema versions at both levels. Decide whether one malformed item rejects the whole JSON document, becomes a per-item error, or prevents asynchronous acceptance. A syntactically invalid JSON body cannot usually produce per-item outcomes because no array exists to process. Separate transport parsing, envelope validation, and business validation in both cases and reporting.
For file imports, cover delimiter, quoting, encoding, line endings, headers, blank rows, truncated rows, and formula-like cells according to the supported contract. Use synthetic content and scan generated error files for accidental exposure of secrets or unrelated rows.
4. Verify Correlation, Ordering, and Result Reconciliation
Every result must map back to an input even if workers reorder processing. Prefer a client-supplied stable key that is unique within the request, plus a server-generated item or operation ID. Array index alone is fragile when the service removes invalid entries, partitions work, or streams results.
Test input order that differs from natural database order. Use keys such as item-30, item-10, and item-20, then verify whether the response preserves input order or explicitly does not guarantee order. Do not fail a test for reordering unless order is contractual. Instead compare results by correlation key and assert uniqueness and completeness.
Reconcile at three levels:
- Submission: number received, rejected at envelope, and accepted for processing.
- Execution: succeeded, failed terminally, retrying, skipped, canceled, and still pending.
- Effects: records written, events produced, notifications requested, and compensations applied.
Counts should form documented equations. For a terminal best-effort job, submitted = succeeded + failed + skipped may be correct. For a running job, pending and retrying must be included. If duplicates are coalesced, the API should expose input count and unique operation count separately so reconciliation does not look like data loss.
Inspect per-item error structure. It should identify the item safely, use stable machine-readable codes, explain actionable validation details, and avoid internal stack traces or other customers' data. Use API error handling and negative testing to align transport and domain errors.
Finally, compare status with durable state. A job endpoint can say 100 succeeded while only 99 records exist because one write was overwritten or a duplicated result was counted twice. Query through supported read APIs or a controlled database view and join by test run ID.
5. Test Validation and Authorization at Two Levels
Envelope authorization asks whether the caller may invoke the bulk function at all. Item authorization asks whether the caller may act on each target. Passing the first must not imply blanket access to every object named in the payload. This is especially important for cross-tenant IDs, administrator tools, bulk exports, and imports that can assign owners or roles.
Create a request containing an owned record, another user's record, and a record from another tenant. Verify the documented atomic or partial behavior, then inspect all three resources. Do not let an error body disclose protected names, existence, owners, or validation details for inaccessible objects. Sometimes the safest item result is a generic not-found code.
Protected properties also need per-item coverage. Mix a permitted profile update with another item attempting to set role, tenantId, or an approval flag. Verify that no serializer or mapping shortcut applies those fields. If the service supports different operation types in one batch, authorize each operation rather than trusting a top-level scope.
Validation ordering affects information disclosure. An unauthorized caller should not learn that a protected object's email format or current version is invalid. The service can authenticate the request, resolve authorization safely, and then apply business validation without returning private context. QA should test visible behavior, not prescribe a universal internal order when architecture requires another safe approach.
For service-to-service batches, test scopes, audience, issuer, expiration, and delegated tenant context. A worker credential with broad infrastructure permission still needs a business rule for the initiating user when it performs user-requested work. Testing broken access control covers the broader authorization model.
6. Exercise Idempotency, Duplicates, and Retry Windows
Bulk requests amplify duplicate risk. A client can time out after the server commits but before the response arrives, then retry the complete request. A queue can redeliver a message. A worker can crash after a side effect and before acknowledging completion. Test each ambiguous boundary.
Clarify the idempotency scope: key plus tenant, key plus endpoint, or key plus authenticated client. Define retention, payload mismatch behavior, concurrent duplicate behavior, and whether the stored result includes per-item outcomes. Reuse the same key with the identical payload, then with one changed item. A changed payload should not silently reuse the old result. It commonly receives a conflict or a documented validation error.
Test duplicates inside one payload separately from repeated requests. Two identical item keys in the same array may be rejected, coalesced, or processed independently. Two different keys targeting the same business object may conflict at the domain layer. Make the expected rule explicit.
Force a client-side timeout only in an authorized test environment. Let the server continue, retry with the same idempotency key, and reconcile records and events. Then retry with a new key to show whether business-level uniqueness prevents duplicate effects. The API idempotency testing guide provides more detailed failure timing patterns.
For partial jobs, define retry granularity. If only failed items are retried, already successful items must not repeat. If the entire job replays, every item handler must be safe or de-duplicated. Verify attempt counters, stable operation IDs, final result selection, and whether a late first attempt can overwrite a successful retry.
Time-based expiry tests should use controllable clocks or short configured test windows, not long sleeps. Check just before and just after the boundary with server-observed time. Document whether a key can be reused after expiry and what business uniqueness still protects.
7. Cover Concurrency, Dependencies, and Isolation
Send two batches that touch the same records. Use overlapping updates, duplicate creates, inventory reservations, balance changes, and delete-versus-update combinations. Synchronize requests with a test barrier so overlap is intentional, then assert the documented winner, conflict, or serialized result. Merely launching promises together does not guarantee the critical sections overlap.
Watch for lost updates. If two items read version 7 and both write version 8, one change may disappear unless the service uses locking, optimistic concurrency, or a commutative operation. Where version preconditions exist, mix current and stale versions and verify per-item conflicts. HTTP 409 Conflict testing helps distinguish state conflicts from malformed input.
Ordering matters when items depend on each other. A batch that creates a customer and then creates that customer's order must declare whether input order is honored, dependencies are expressed, or such combinations are invalid. Test forward references, cycles, missing parents, and a failed prerequisite. A dependent item should be skipped or failed predictably, not run against partial state.
Isolation also includes other clients. While a long atomic batch is running, read touched and untouched records. Verify whether intermediate states are visible according to the database isolation and product contract. For best-effort work, partial visibility may be expected, but summaries and caches must not claim terminal completion early.
Cancellation races deserve specific cases: cancel before start, during processing, after the final item but before status publication, and after completion. Define whether in-flight items finish, stop, or compensate. Repeated cancellation should be safe. The terminal record should retain enough information to reconcile which items committed.
Use unique test run IDs and avoid shared production-like quotas. Concurrency tests can be destructive if they reserve scarce inventory or send real messages. Stub external delivery only when the purpose is internal consistency, then run a smaller integrated case against approved sandboxes.
8. Test Asynchronous Job Lifecycle and Recovery
A 202 response should include or reference a stable job identifier, accepted representation, and a way to learn progress. Validate location headers or links, authorization on status and result endpoints, initial state, safe polling guidance, and behavior for unknown or expired jobs. Job IDs must not expose another tenant's results.
Verify every allowed state transition. A queued job may move to running, then completed, partially completed, failed, or canceled. A retry can create an attempt beneath the same job or a new job, but the public model must remain coherent. Timestamps should be ordered and use a documented time representation. Percent complete should not decrease unless restart behavior explicitly permits it.
Test worker interruption at controlled checkpoints. Stop after acceptance but before the first item, after a commit but before progress update, and near terminal publication. Restart the worker and verify recovery from durable checkpoints. The system should not lose accepted work or double-apply committed items.
Result retention is functional and security behavior. Confirm who can download an error file, whether URLs expire, how filenames are sanitized, when artifacts are deleted, and what status remains afterward. A completed job may retain counts longer than row-level error data. Verify the stated policy without using customer data.
For callbacks, test signature verification, duplicate delivery, out-of-order delivery, receiver downtime, and retry exhaustion. The job should remain queryable even if notification fails unless the contract deliberately couples them. Keep processing outcome separate from notification outcome in observability and UI messages.
Finally, test operational recovery. Replaying a dead-letter message or manually retrying a job should require authorization, create an audit record, preserve the original request identity, and use safe de-duplication. A support control that fixes jobs but bypasses tenant policy creates a new risk.
9. Evaluate Limits, Performance, and Backpressure
Limits are part of the API contract, not only a performance concern. Determine maximum item count, request bytes, per-item bytes, processing duration, concurrent jobs, per-tenant quota, and result size. Test exact boundaries and make error responses actionable. A gateway body limit may fire before the application item limit, so verify the actual external behavior.
Measure several shapes: few large items, many small items, validation-heavy invalid items, write-heavy valid items, and mixed outcomes. Record client latency, server processing time, queue wait, throughput, error rate, resource use, and downstream pressure. Do not publish a universal target. Compare against the service-level objective and capacity model agreed for the system.
Gradually increase load in a production-like authorized environment. A single enormous request gives little information about the knee where latency or errors change. Hold payload shape constant while varying concurrency, then hold concurrency constant while varying size. Include noisy-neighbor cases across tenants if isolation is a requirement.
Backpressure should be explicit. The service may reject with 429, limit concurrent jobs, queue within a bounded depth, or ask clients to retry later. Verify retry guidance and avoid synchronized immediate retries. See API rate limiting testing for fair-use and recovery cases.
Check that failure remains bounded. An oversized request should not exhaust memory, block health checks, or create a job that can never finish. Error-file generation must also have limits. A million validation messages can be more expensive than processing the data, so verify truncation and summary behavior when documented.
Performance tests need reconciliation. High throughput is meaningless if results are missing or duplicated. Tag every item with the run ID, count durable effects, and sample field correctness. Clean up in a controlled way that does not hide delayed work still arriving after the test ends.
10. Automate Testing Bulk and Batch Endpoints
The following self-contained Node.js 20+ test models a best-effort credit endpoint. It validates item keys, authorizes each account by tenant, de-duplicates repeated idempotency keys, and returns one result per input. Save it as batch.test.mjs, then run node --test batch.test.mjs.
import test from 'node:test';
import assert from 'node:assert/strict';
function createProcessor() {
const balances = new Map([['a-1', 10], ['a-2', 20], ['b-1', 30]]);
const tenants = new Map([['a-1', 'alpha'], ['a-2', 'alpha'], ['b-1', 'beta']]);
const completed = new Map();
function process({ tenant, idempotencyKey, items }) {
if (completed.has(idempotencyKey)) return completed.get(idempotencyKey);
const seen = new Set();
const results = items.map(item => {
if (!item.key || seen.has(item.key)) return { key: item.key ?? null, code: 'DUPLICATE_KEY' };
seen.add(item.key);
if (!Number.isInteger(item.amount) || item.amount <= 0) return { key: item.key, code: 'INVALID_AMOUNT' };
if (tenants.get(item.accountId) !== tenant) return { key: item.key, code: 'NOT_FOUND' };
balances.set(item.accountId, balances.get(item.accountId) + item.amount);
return { key: item.key, code: 'SUCCEEDED' };
});
const response = {
submitted: items.length,
succeeded: results.filter(result => result.code === 'SUCCEEDED').length,
failed: results.filter(result => result.code !== 'SUCCEEDED').length,
results,
};
completed.set(idempotencyKey, response);
return response;
}
return { process, balance: id => balances.get(id) };
}
test('returns correlated partial results and protects another tenant', () => {
const api = createProcessor();
const result = api.process({
tenant: 'alpha',
idempotencyKey: 'run-1',
items: [
{ key: 'ok', accountId: 'a-1', amount: 5 },
{ key: 'bad', accountId: 'a-2', amount: 0 },
{ key: 'foreign', accountId: 'b-1', amount: 50 },
],
});
assert.deepEqual([result.submitted, result.succeeded, result.failed], [3, 1, 2]);
assert.deepEqual(result.results.map(item => item.code), ['SUCCEEDED', 'INVALID_AMOUNT', 'NOT_FOUND']);
assert.equal(api.balance('a-1'), 15);
assert.equal(api.balance('b-1'), 30);
});
test('same idempotency key does not apply successful items twice', () => {
const api = createProcessor();
const request = {
tenant: 'alpha',
idempotencyKey: 'run-2',
items: [{ key: 'credit', accountId: 'a-2', amount: 7 }],
};
assert.deepEqual(api.process(request), api.process(request));
assert.equal(api.balance('a-2'), 27);
});
test('duplicate correlation keys are explicit', () => {
const api = createProcessor();
const result = api.process({
tenant: 'alpha',
idempotencyKey: 'run-3',
items: [
{ key: 'same', accountId: 'a-1', amount: 1 },
{ key: 'same', accountId: 'a-2', amount: 1 },
],
});
assert.deepEqual(result.results.map(item => item.code), ['SUCCEEDED', 'DUPLICATE_KEY']);
assert.equal(api.balance('a-2'), 20);
});
This sample is intentionally small. A real test should call the deployed API, use durable reads, and verify events. It should also test payload mismatch under one idempotency key, simultaneous duplicates, and failures after commit. The value of the model is its explicit per-item oracle and reconciliation, not the in-memory implementation.
Interview Questions and Answers
Q: What do you clarify before testing a bulk API?
I clarify atomicity, item identity, ordering, limits, validation layers, authorization, idempotency, retry granularity, timeout, and result correlation. If it is asynchronous, I also define job states, cancellation, recovery, retention, and notification behavior.
Q: How do you test partial success?
I mix valid and controlled invalid items at different positions, then map each output by correlation key. I reconcile counts and durable state, and verify one failure neither changes that item nor incorrectly blocks independent valid items.
Q: How do you test an atomic batch?
I snapshot every affected resource, introduce a failure early, middle, and late, and verify complete rollback of business records and downstream side effects. I also include a fully valid positive request so a broken endpoint does not pass by rejecting everything.
Q: Why is response order a weak oracle?
Parallel workers may complete out of order, and many contracts do not promise input ordering. I correlate by a unique client key or operation ID, then assert completeness, uniqueness, and the documented order rule separately.
Q: What idempotency cases matter for bulk requests?
I repeat the identical request with the same key, send a changed payload with that key, race simultaneous duplicates, retry after client timeout, and cross the retention boundary. I reconcile all item effects because one repeated success can be hidden inside correct aggregate counts.
Q: How do you test authorization in a batch?
I verify permission to invoke the endpoint and permission for each target and operation. I mix owned, unrelated, and cross-tenant items, then confirm denied items reveal no protected metadata and produce no side effect.
Q: What does 202 Accepted prove?
It proves that the server accepted the request for processing according to its initial contract, not that business processing succeeded. I follow the job through a secured status or result channel and reconcile terminal outcomes.
Q: How do you performance-test a bulk endpoint?
I vary item count, bytes, payload shape, and concurrency independently, measure latency and queue behavior against agreed objectives, and verify backpressure. Every load run includes data reconciliation so throughput cannot hide loss or duplication.
Common Mistakes
- Checking only an all-valid payload and calling the endpoint covered.
- Assuming 200 means every item succeeded or 202 means the job completed.
- Failing on response reordering when the contract never guarantees order.
- Omitting stable item keys, which makes retries and reconciliation ambiguous.
- Testing item count but ignoring total bytes and error-result size.
- Verifying per-item messages without checking committed records and downstream events.
- Reusing one idempotency key across tenants or unrelated test cases.
- Authorizing the envelope but never crossing ownership and tenant boundaries per item.
- Running concurrency tests without controlled overlap or unique data.
- Cleaning data before delayed workers finish, then misreading late results as product defects.
Conclusion
Testing bulk and batch endpoints is a data-integrity exercise built around explicit processing boundaries. Decide whether work is atomic, partial, or asynchronous, identify every item, reconcile each terminal outcome, and test the ambiguous points where clients retry or workers recover.
Start with a three-item request: one valid, one invalid, and one unauthorized. Trace each item from submission through response, storage, events, and final status. That compact case will reveal whether the contract is observable enough to support larger limits, concurrency, and recovery testing.
Interview Questions and Answers
What is your first step when testing a batch endpoint?
I identify whether it is synchronous or asynchronous and whether the transaction is atomic, per-item, or partitioned. Then I document item correlation, limits, retries, ordering, authorization, and the durable result channel before designing cases.
How would you validate mixed results in a bulk response?
I create a manifest of input keys and expected outcomes, map responses by key, and assert exactly one terminal result per input. I reconcile aggregate counts and verify persistent state and events for both successful and failed items.
What failures do you place inside an atomic request?
I use validation, authorization, conflict, and controlled infrastructure failures at early, middle, and late positions. After each, I verify complete rollback of the documented transaction and no leaked side effects.
How do you test duplicate items?
I distinguish duplicates inside one payload, repeated business targets with different item keys, and a complete request retried with one idempotency key. Each needs an explicit expected outcome and durable effect check.
How do you test an asynchronous batch worker crash?
I interrupt at controlled checkpoints, including after commit but before acknowledgment, restart the worker, and verify checkpoint recovery. The final job must neither lose accepted work nor double-apply an item.
What would you monitor for a batch API?
I monitor request rejection, accepted jobs, queue age, processing duration, retry and dead-letter counts, per-item failure codes, terminal job states, and reconciliation gaps. Metrics are segmented by safe tenant or workload dimensions without exposing item data.
How do you test batch cancellation?
I cancel before start, during work, near terminal publication, after completion, and repeatedly. I verify the defined behavior for in-flight items, terminal state, committed effects, compensation, and audit trail.
How do you avoid flaky bulk API tests?
I use unique item and run identifiers, isolate quotas and records, correlate without assuming order, wait on observable job states, and avoid fixed sleeps. Cleanup runs only after terminal reconciliation so delayed work cannot contaminate another case.
Frequently Asked Questions
What is the difference between a bulk endpoint and a batch endpoint?
A bulk endpoint usually accepts several operations in one API request. Batch processing often describes grouped work that may run asynchronously, use workers, and expose a job lifecycle. The product contract matters more than the label.
Should a bulk API be atomic?
It depends on the business invariant. Financial or tightly related changes may require all-or-nothing behavior, while independent imports may benefit from per-item outcomes. The API must document the boundary so clients can recover safely.
How should partial success be returned?
Return a stable correlation key and machine-readable result for every input item, plus aggregate counts that reconcile. The exact HTTP status and envelope can vary, but clients must be able to identify retryable and terminal outcomes.
What should QA verify after a 202 response?
Verify the secured job reference, lifecycle transitions, final per-item results, durable side effects, retries, and retention behavior. A 202 response alone confirms neither item validation nor business success.
How do you test maximum batch size?
Test empty, one, normal, exact maximum, and maximum plus one for item count, then test byte limits separately. Confirm the external error, absence of partial work when rejected, and stability of health and memory.
How do idempotency keys work with batches?
The service associates a key with a request scope and retains its outcome for a defined window. Tests should repeat identical and changed payloads, race duplicates, and verify that successful items are not applied twice.
How can QA reconcile a large batch?
Tag the job and items with stable synthetic identifiers, export or query outcomes through supported channels, and compare submitted, succeeded, failed, skipped, pending, records, and events. Sample field correctness in addition to counting rows.