STORAGE_LIMIT_EXCEEDED: storage limit exceeded
Your org has hit its data storage or file storage cap. New record creation fails until you free space or buy more storage. Audit which objects are eating storage; archive or delete what you don't need.
Also seen asSTORAGE_LIMIT_EXCEEDED·storage limit exceeded·Data storage limit reached·File storage limit
The integration that uploads case attachments from a partner portal has been running for two years. This week, every upload starts returning STORAGE_LIMIT_EXCEEDED: storage limit exceeded. The disk monitoring dashboard for the partner portal looks fine. The Salesforce org's storage usage page tells a different story: 99.7% full, with the curve climbing.
What the platform actually tracks
A Salesforce org has two separate storage allocations: Data Storage and File Storage. They are tracked, billed, and limited independently. The error message STORAGE_LIMIT_EXCEEDED can come from either, and the fix depends on which one is full.
Data Storage holds records: rows in standard and custom objects, including Account, Opportunity, Case, and every Object__c you created. Each record counts as 2 KB regardless of how many fields it has. Long text fields, rich text fields, and large picklists do not push individual records above the 2 KB count. Storage is measured by row count, not by byte size of the data.
File Storage holds the file-shaped data: ContentVersion (Files), Attachment (legacy attachments), Documents, and email attachments. The actual byte size of each file counts against File Storage. A 10 MB PDF uses 10 MB; a 50 KB image uses 50 KB. The limit is the sum of all those bytes.
Default allocations are 10 GB Data Storage for Enterprise editions plus a per-user additive (around 20 MB per user), and 10 GB File Storage plus a smaller per-user additive. Numbers vary by edition. Setup, Storage Usage shows your org's exact allocation and current usage.
The broken example
A simple integration that uploads a partner's case attachment to Salesforce:
@RestResource(urlMapping='/case-attachment/*')
global class CaseAttachmentRestController {
@HttpPost
global static String upload() {
RestRequest req = RestContext.request;
String caseId = req.requestURI.substringAfterLast('/');
Blob body = req.requestBody;
String filename = req.headers.get('X-Filename');
ContentVersion cv = new ContentVersion(
Title = filename,
PathOnClient = filename,
VersionData = body,
FirstPublishLocationId = caseId
);
insert cv;
return cv.Id;
}
}
When File Storage hits the limit, the insert cv line throws STORAGE_LIMIT_EXCEEDED: storage limit exceeded. The partner portal receives a 500 response. The portal's retry logic re-uploads the same file. Every retry hits the same wall.
A second shape, on the Data Storage side: a batch job that creates a Case_Activity_Log__c row for every case touch. Multiply by ten years of operations and the org has 12 million Case_Activity_Log__c rows. At 2 KB each, that's 24 GB of Data Storage. The next batch run throws STORAGE_LIMIT_EXCEEDED: storage limit exceeded on the first insert.
A third shape: an email-to-case integration that captures every inbound email as an EmailMessage record plus an Attachment per file. Six years of customer-support emails generate millions of EmailMessage rows and a few terabytes of attachments. Once the threshold passes, every new case email fails.
What the platform is actually checking
When a DML insert (or upsert with a new row) is about to commit, the platform checks the org's current storage usage against the allocation. If the operation would push usage above the limit, the DML is rejected with STORAGE_LIMIT_EXCEEDED.
The check happens at commit, after savepoints and triggers run. That's why partial-success operations like Database.insert(records, false) can succeed for some rows and fail for others when the limit is hit mid-batch.
There is no graceful degradation. The platform doesn't slow down or warn first. The error fires the moment the limit is crossed. Integrations that don't handle the error end up in retry loops that never recover.
Storage usage is computed asynchronously. The Storage Usage page in Setup might lag actual usage by an hour. If you've been deleting records to free space and the error keeps firing, the page might not yet reflect the deletes. Wait an hour, or check the count of the offending object directly via SOQL.
The fix, ordered by likelihood
Free space by deleting old records and files. The fastest way out of a storage incident is to remove what doesn't need to be there. Run a SOQL query to find the biggest object by row count:
SELECT COUNT() FROM Case_Activity_Log__c WHERE CreatedDate < LAST_N_YEARS:3
If the count is large, delete via the Data Loader, the Bulk API, or a scheduled Apex batch. Hard-delete (skip the Recycle Bin) to free the space immediately; soft-deleted records still count against storage for 15 days.
For File Storage, find the biggest ContentVersion records:
SELECT Id, Title, ContentSize FROM ContentVersion ORDER BY ContentSize DESC LIMIT 100
Identify large files that no longer need to be in Salesforce (export them to S3, an internal NAS, or your DMS), delete from Salesforce, then empty the Recycle Bin.
Move files to external storage. Files that don't need to be searched or accessed directly from Salesforce can live in external storage. Salesforce Files Connect, Box, S3, Google Drive, OneDrive. Each integrates with Salesforce so that records can reference external files without storing the file bytes in Salesforce's File Storage.
For new files going forward, use a Files-Connect integration so the partner upload writes to S3 (or wherever) and stores only the metadata pointer in Salesforce. The 10 MB file uses 0 bytes of File Storage.
Buy more storage. Salesforce sells additional Data Storage and File Storage as add-ons. Pricing varies by edition. For orgs where the data legitimately needs to be in Salesforce (regulatory retention requirements, active customer-support workflows), buying more storage is the right answer.
The trick is to combine all three: delete what you don't need, externalize what doesn't need to be in Salesforce, then buy headroom for what remains.
The fixed example
A revised upload controller that writes large files to S3 and only stores small files in Salesforce:
@RestResource(urlMapping='/case-attachment/*')
global class CaseAttachmentRestController {
private static final Integer SMALL_FILE_THRESHOLD = 1024 * 1024; // 1 MB
@HttpPost
global static String upload() {
RestRequest req = RestContext.request;
String caseId = req.requestURI.substringAfterLast('/');
Blob body = req.requestBody;
String filename = req.headers.get('X-Filename');
if (body.size() > SMALL_FILE_THRESHOLD) {
String s3Url = S3Uploader.uploadAndGetSignedUrl(filename, body);
Case_Attachment_Pointer__c ptr = new Case_Attachment_Pointer__c(
Case__c = caseId,
Filename__c = filename,
External_URL__c = s3Url,
Size_Bytes__c = body.size()
);
insert ptr;
return ptr.Id;
}
ContentVersion cv = new ContentVersion(
Title = filename,
PathOnClient = filename,
VersionData = body,
FirstPublishLocationId = caseId
);
insert cv;
return cv.Id;
}
}
Small files stay in Salesforce. Large files go to S3 with a pointer record in Salesforce that references the S3 URL. The pointer record uses 2 KB regardless of the underlying file size, so a 10 MB file consumes 2 KB of Data Storage and 10 MB of S3 storage.
Detecting the problem before it fires
The Storage Usage page (Setup, Data Management, Storage Usage) is the primary monitoring surface. It shows current usage by category, top objects by row count, and top users by file storage.
For programmatic monitoring, query the Organization standard object:
List<Organization> orgs = [
SELECT Id, Name, IsSandbox
FROM Organization
];
The Organization object doesn't expose storage directly, but a Tooling API call to organization or apexResource endpoints can return usage data. Most teams instead schedule a daily SOQL count against the top three or four highest-row-count objects and alert when growth exceeds a threshold.
The Lightning Reports surface lets you build a report on ContentVersion ordered by ContentSize desc. Pin the report to an admin dashboard. Anomalies (a sudden spike in upload size) become visible early.
Big Objects for high-volume data
For data that legitimately needs to stay in Salesforce but doesn't need full record functionality (no Chatter, no field history, no triggers), Big Objects are designed for it. A Big Object can hold billions of rows and the rows don't count against standard Data Storage.
Conversion from a custom object to a Big Object is non-trivial: Big Objects use a different query syntax (async SOQL or SOQL with composite keys), don't support all field types, and don't show up in standard reports without extra configuration. For audit logs, historical case data, telemetry, or any append-only high-volume table, the migration is usually worth it.
When the problem is a runaway integration
Sometimes the storage growth is a single misbehaving integration: a webhook that creates duplicate records, a batch job that fires on every record save, a process builder that inserts a "log" record on every update.
Find it by querying CreatedBy on the fast-growing object:
SELECT CreatedById, COUNT(Id)
FROM Case_Activity_Log__c
WHERE CreatedDate = LAST_N_DAYS:7
GROUP BY CreatedById
ORDER BY COUNT(Id) DESC
The user (often the Automated Process user or an integration user) at the top of the count is the culprit. Pause the integration, audit its logic, and clean up the historical mess before resuming.
Closely related errors
| Error | Cause |
|---|---|
STORAGE_LIMIT_EXCEEDED: storage limit exceeded | Data or File Storage allocation full |
REQUEST_RUNNING_TOO_LONG | Too much data in transaction; not strictly storage but related |
FILE_TOO_LARGE | Single file exceeds the maximum size (37 MB for Visualforce, larger for Bulk API) |
TOO_MANY_DML_ROWS | Single transaction tried to insert more than the per-transaction governor allows |
All four are about volume, and all four respond to "delete, externalize, or upgrade."
Why the Recycle Bin matters during cleanup
Records deleted via standard DML or the Data Loader land in the Recycle Bin for 15 days. Those records still count against Data Storage until they are emptied. If you delete 50 GB of data trying to recover from a STORAGE_LIMIT_EXCEEDED incident and don't empty the Recycle Bin afterward, the storage page still shows the org full and the next insert still fails.
Empty the Recycle Bin from the App Launcher or programmatically with Database.emptyRecycleBin(ids). For bulk cleanup, the Data Loader supports a "Hard Delete" operation that bypasses the Recycle Bin entirely. Use it when you're confident the records don't need to be recoverable.
Sandboxes have a smaller Recycle Bin retention window (sometimes 24 hours) but the same principle applies: the bin counts against storage until it's emptied.
Defensive habits
Schedule a quarterly storage audit. Top objects by count, top users by file size, growth rate over the previous quarter. Action items come from comparing the numbers to the org's allocation.
Set a retention policy in writing. "Case_Activity_Log__c older than 24 months gets deleted." "ContentVersions older than 5 years and not referenced by an active record get archived." Schedule batch jobs that enforce the policy automatically.
Externalize anything that doesn't need to be in Salesforce. Documents that customers download but never edit in Salesforce. Telemetry that's only queried in aggregate. Email backups. All belong in cheaper external storage.
Monitor storage as a first-class metric. Alert when usage crosses 80% and when growth exceeds the historical baseline. The earlier the warning, the cheaper the cure.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Governor limit errors
Apex code is approaching a governor limit warning email
Governor limitSalesforce sent your team an email saying Apex is approaching a governor limit (typically 80% of CPU, SOQL, DML, or callout caps) but didn't…
System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
Governor limitYou did a DML statement and then tried to make an HTTP callout in the same synchronous transaction. The platform forbids this because the ca…
System.LimitException: Apex CPU time limit exceeded
Governor limitYour transaction spent more than 10 seconds (sync) or 60 seconds (async) of CPU time inside Apex code. This counts compute, not waiting — SO…
System.LimitException: Apex heap size too large
Governor limitYour transaction is holding more data in memory at once than Apex allows: 6 MB synchronous, 12 MB asynchronous. Usually it's a `List<SObject…
System.LimitException: Maximum trigger depth exceeded
Governor limitTriggers can fire other triggers, which can fire more triggers — but only 16 levels deep. Hit 17 and the platform stops the whole transactio…