Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

Short answer: PK chunking splits a Bulk API 1.0 query into a series of batches bounded by record ID ranges rather than by row counts, so Salesforce never has to hold a single enormous result set in memory. You turn it on with a request header when you create the job. Bulk API 2.0 does the same thing automatically and gives you no header to set, which is why this is a topic about the older API that is still very much in use.

How the chunking works

Salesforce takes the object’s record IDs, walks them in order, and cuts the range into slices of a size you nominate. Each slice becomes its own batch with an implicit WHERE Id >= x AND Id < y appended to your query. The batches then process in parallel like any other Bulk 1.0 batches.

You enable it on the job creation call:

Sforce-Enable-PKChunking: chunkSize=150000

That is the whole configuration in the simple case. The job comes back with batches appearing over the next little while as Salesforce works out the ranges, which is why a PK chunked job shows zero batches immediately after creation and then suddenly shows forty.

The important detail: chunks are defined by ID range, not by row count. IDs are not evenly distributed, particularly on an object where large volumes have been deleted or where records were loaded in bursts. A chunk size of 100,000 does not give you batches of 100,000 rows. It gives you batches covering an ID range that would hold 100,000 rows if the space were full, and some of them will come back nearly empty.

Choosing a chunk size

The default is 100,000 and the maximum is 250,000. Bigger chunks mean fewer batches and less coordination overhead. Smaller chunks mean more parallelism and smaller units to retry when one fails.

Start at the default. Move up towards the maximum when your batches are completing comfortably and you have a very large object, because fewer batches means less polling and fewer result files to stitch together. Move down when batches are timing out, which usually means the query is doing more work per row than you thought, or when a formula field or a complex filter is making each row expensive.

Batch failures are per batch. One failed batch does not lose the job, and you can reissue just that ID range as a separate query. Keeping chunks moderate makes that recovery cheap.

The parent option

Sharing and history objects do not have usefully distributed IDs of their own, so chunking them directly works badly. For those, name the parent:

Sforce-Enable-PKChunking: chunkSize=100000; parent=Account

Salesforce then chunks by the parent object’s IDs and pulls the related share or history rows for each range. This is the only sensible way to extract a large AccountShare or a custom object’s share table.

There is also a startRow parameter that lets you begin from a specific ID, which matters when you are resuming an extract that failed partway and you do not want to redo the first forty batches.

What it supports

All custom objects, and a list of standard ones that covers the high-volume cases: Account, Contact, Lead, Opportunity, Case, Campaign, CampaignMember, Task, User, LoginHistory, plus share and history objects through the parent option. Objects outside that list will reject the header or ignore it.

When not to bother

If you are writing a new extract, use Bulk API 2.0 query and stop thinking about this entirely. Version 2.0 chunks server side, hands you results through a locator, and removes the whole decision. PK chunking matters when you are maintaining an existing 1.0 pipeline, when your ETL tool only speaks 1.0, or when you specifically want control over the ID ranges for restartability.

It also does nothing for a query that is slow because of a poor filter. Chunking splits the work. It does not make a full table scan selective.

Frequently asked

Why are some of my batches empty? Because the ID range they cover contains no live records. Perfectly normal, especially after a large delete. Empty batches complete instantly and cost you nothing.

Can I use PK chunking on an ingest job? No. It is a query-side feature only.

Does it help with query timeouts? Usually yes, because that is the problem it was built for. A query that times out as a single batch often succeeds as forty smaller ones.

What happens if records are created while the job runs? Rows with IDs beyond the last chunk boundary are not included. Treat the extract as a snapshot taken at submission time, and pick up the remainder with an incremental pass.