Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Diagram illustrating how the Salesforce @ReadOnly annotation increases Apex query row limits for large data volumes
Apex

Optimize Salesforce query performance with @ReadOnly

Tired of hitting that 50,001-row limit in Apex? Here is how the @ReadOnly annotation bumps your query capacity up to a million rows. It is a small change that makes a big difference for large data volumes.

If you've ever hit the dreaded SOQL limit while trying to pull a massive report or export, you know how much it can hurt your Salesforce query performance. You're trying to process a large dataset, and suddenly the platform throws a 50,001-row error in your face. It's frustrating, and there's a specific tool in the Apex toolkit that many developers overlook.

I'm talking about the @ReadOnly annotation. It's a simple little tag that bumps your query limit from 50,000 rows all the way up to 1,000,000. If you are already managing Salesforce large data volumes, this is one of the easiest ways to get a quick win.

What @ReadOnly actually does for Salesforce query performance

So what does this mean for your code? When you mark a method as @ReadOnly, you're telling Salesforce, "I promise I won't change anything; I just need to look at the data." Because you're giving up the ability to make changes, Salesforce relaxes the governor limits. It's a fair trade-off when you need to crunch numbers or build a heavy-duty dashboard.

One thing that trips people up is where you can actually use it. You can't just throw it on any old trigger or controller. It's designed for specific contexts like REST and SOAP web services, the Schedulable interface, and certain script-based entries. I've seen teams try to use it in triggers to bypass limits and get a compilation error for their trouble.

Architecture diagram of the data flow for scheduled jobs and web services in a Salesforce org.

Architecture diagram of the data flow for scheduled jobs and web services in a Salesforce org.

The "No-Go" list: restrictions you can't ignore

There's no such thing as a free lunch in Salesforce. If you want that massive 1-million-row limit to boost your Salesforce query performance, you have to follow some strict rules. Here's the catch:

  • No DML allowed. You can't insert, update, or delete a single record. If you even try to call update myAcc; inside a read-only context, the whole thing will blow up.
  • No async calls. You can't fire off a @future method or enqueue a Queueable job. Salesforce wants to keep this transaction strictly about reading data.
  • Specific contexts only. It works best in web services, or when you're using the @AuraEnabled annotation for Lightning components, provided you set the cacheable attribute correctly.

If you find yourself needing to update records after reading a million rows, don't try to force it into one transaction. I usually suggest pulling the data in a read-only call and then passing the IDs to a separate Batch Apex job for the actual processing.

Best practices for Salesforce query performance and data handling

Just because you can query a million rows doesn't mean you should do it recklessly. I've seen developers write wide-open queries that eat up the heap limit before they even get to the 100,000th row. You still have to be smart about how you write your SOQL.

Always filter your queries as much as possible. Use indexed fields in your WHERE clause and only select the fields you actually need. If you're building something for a modern UI, you might also want to look into Apex Cursors to handle these big chunks of data more gracefully. The aim is to keep the overhead low while you get the most out of your Salesforce query performance.

Example: using @ReadOnly in a controller

Here is a quick look at how you'd actually set this up in a class. Notice how simple it is to implement once you have the right context.

public with sharing class DataExportController {
    @AuraEnabled(cacheable=true)
    @ReadOnly
    public static List<Account> getLargeAccountList() {
        // This can now return up to 1 million rows
        return [SELECT Id, Name, AnnualRevenue FROM Account WHERE CreatedDate = LAST_YEAR];
    }
}

The heap limit is still waiting for you, though. If those million rows are beefy records with dozens of fields, you'll run out of memory long before you hit the row limit. I always tell my junior devs: "The row limit is a ceiling, but the heap limit is the floor. Watch both."

Key takeaways

  • The @ReadOnly annotation is your best friend for 1,000,000-row SOQL queries.
  • It completely forbids any DML operations (Insert, Update, Delete).
  • It's built for reporting, analytics, and data exports where Salesforce query performance matters.
  • You can't use it to trigger other asynchronous processes.
  • Heap limits still apply, so don't query more fields than you need.

Honestly, most teams get this wrong by trying to use it in the wrong places. But if you're building a read-only integration or a heavy data dashboard, it's a lifesaver. It keeps your Salesforce query performance high without hitting those restrictive standard limits. Just remember to keep your logic clean and your queries selective.

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment