Moodle SCORM progress resets past the halfway mark? suspend_data is truncated in two places, not one
The advice everyone repeats (“disable SCORM standards mode”) addresses half the problem and often doesn't restore resume, because the learner's bookmark is being cut twice: once inside the course package, once inside the LMS. Here's how to find both cuts, prove which one is yours in the console, and fix them in the order that won't cost you the audit.
cmi.suspend_data being truncated past the SCORM 1.2 spec limit of 4,096 characters. The catch: that limit can be enforced in two independent places. One is the authoring tool's own client-side API wrapper, which can trim the value before it ever leaves the browser. The other is Moodle's own cap, a site setting called Enable SCORM 1.2 standard mode that holds packages to the 1.2 limit. It ships ticked, so out of the box Moodle enforces 4,096 as well; untick it and Moodle will store up to 64,000. But that lifts only one of the two caps. If the authoring package trimmed cmi.suspend_data in the browser first, the data was gone before Moodle ever saw it, and the bigger server limit has nothing left to keep. This is a data-loss problem, not a status problem; if your course instead shows “incomplete” after a clean finish, that's a different failure with different fixes.The ticket came in on a Thursday. A 214-slide bloodborne-pathogens refresher, built in Articulate Storyline 360, published to SCORM 1.2, sitting in Moodle 4.4, due for 340 warehouse and dock staff before an OSHA re-audit the following month. Forty-one of them had gotten past slide 128, closed the tab for the night, came back, and landed on slide 1. Not “resume from where you were”: slide one, timer reset, quiz bank reshuffled.
The training coordinator had already done her homework. She'd found the forum threads and done what they said: opened the SCORM Package settings, hunted down SCORM standards mode, and made sure it was switched off. It made no difference, and the same learners reset the same way the next morning. By the time she called us she was three weekends deep and convinced Moodle was broken. Moodle wasn't broken. The advice was just incomplete.
We measured it in about twenty minutes. The course genuinely needed roughly 6,100 characters of suspend_data to describe a learner who'd reached slide 128: every branch taken, every knowledge-check answered, the exact timeline position. The value actually arriving in the database was 4,096 characters, flat, every time, cut mid-token with the closing brace of the state object missing. Storyline read that back on the next launch, found JSON it couldn't parse, and did the only safe thing a player can do with a corrupt bookmark: it threw it away and started clean. The tail describing the second half of the course had never left the browser. Toggling the Moodle setting couldn't have helped, because the truncation happened one hop earlier, in the package's own JavaScript, before Moodle ever saw the value.
We do this repair often enough to have opinions about it, and the strongest one is this: the popular fix targets the wrong end of the pipe about half the time. So the rest of this walks the pipe end to end, both places the data can be cut, how to prove which cut is yours, and the fix ladder in the order that won't cost you the audit.
First, which problem is this
Three symptoms that sound identical and aren't
Before you touch a single setting, name the symptom precisely, because three very different failures get reported with the same five words: “it lost my progress.” Getting this wrong is how people spend a week republishing packages that were never the problem.
- “It starts over from slide 1.” Progress from the current or a previous session is gone on return; the resume point is missing or wrong. That's truncation (gate one or gate two below), or a resume-versus-new-attempt setting. This is the article you're reading.
- “An earlier section I finished reverted to 0% after I did a later one.” Recent progress holds, old progress reverts. That's advance overwriting, which we get to further down. It looks like a size problem and often isn't.
- “The player said I finished but the LMS still shows incomplete.” The data arrived intact; the status word the package sent, or the completion condition Moodle checks, is the mismatch. That's a status problem, and it has its own guide: why your SCORM course isn't tracking completions. Don't start editing suspend_data for a status bug.
The distinguishing test for this article takes thirty seconds. Have someone reach the halfway mark, close the tab, and reopen. If they resume where they left off but the course won't complete, you're in the status guide. If they resume to the beginning, or to somewhere earlier than they'd reached, you're in the right place. And if you can't tell which bucket you're in from the description alone, that's fine: the diagnostics later make the course tell you directly, in numbers.
The mechanism
What suspend_data is, and the two places it gets cut
When a learner opens a SCORM activity, the course package and Moodle talk through a small run-time interface, the SCORM API object the LMS exposes in the browser. The package doesn't hand Moodle a tidy “here's what they finished.” It stores its entire private notion of the learner's state in one string, cmi.suspend_data, and calls LMSSetValue("cmi.suspend_data", "...") whenever that state changes. Storyline, Adobe Captivate, and Rise all lean on this string heavily: branching paths, per-question responses, timeline positions, which layers a learner has seen. It is the course's memory of the learner, serialized into text.
The SCORM 1.2 spec caps that string at 4,096 characters, and caps most other CMI strings at 255. That was roomy in 2001 and is cramped now; a long branching compliance course generates far more state than that. When the string exceeds the cap the excess is cut and the tail is lost silently: no error to the learner, no warning in the report. The bookmark just comes back malformed, and the player starts over.
That 4,096-character limit isn't enforced in one place. It's enforced in two, on opposite sides of the network, and clearing only the downstream one changes nothing if the upstream one is tighter. Our OSHA course needed about 6,100 characters to hold a learner at slide 128 and was getting exactly 4,096, so something was lopping off roughly a third of the state. The only question is which of the two limits did it.
- Gate one, the client-side cut, inside the package. Every authoring tool ships its own SCORM API wrapper, and many of them enforce the 1.2 spec themselves. In one case documented on moodle.org (discussion 470344, a Moodle 4.5 site running a SCORM 1.2 package), the wrapper stored the value as
value.substring(0, 4096)forcmi.suspend_data(a hard chop), alongside a separate, smaller per-tool size constant. Exact names and numbers vary by tool; the pattern doesn't. This happens in the learner's browser, beforeLMSSetValueis ever called, so Moodle never receives the tail and no Moodle setting can bring it back. - Gate two, the server-side cut, inside Moodle. The value that does make it out is validated again on the way in. SCORM standards mode is a site-level setting on the SCORM Package plugin page, under Site administration → Plugins → Activity modules → SCORM Package. It ships ticked, so by default Moodle enforces the strict SCORM 1.2 limits on write: 255 characters on most strings, 4,096 on
suspend_data. Untick it and Moodle extends these fields to 64,000 characters, the SCORM 2004 maximum. So on a default site this gate is closed at 4,096, and unticking it is a real, necessary step, just not always a sufficient one.
One thing not to conflate: that site-wide setting is a different animal from the per-activity Compatibility settings section (Auto-continue, Force completed, Auto-commit, Mastery score overrides status). Different screen, different job. This is also why the “disable standards mode” advice so often isn't enough on its own. Unticking it does lift Moodle's own cap, and that part is real. But that setting only ever governed the second cut; if your authoring tool made the first cut in the browser, the string was already 4,096 characters by the time it reached the server, whatever the LMS did with its own limit. To actually raise the ceiling you have to clear both caps: get the package to stop capping itself and get Moodle to leave the field alone.
The advice that isn't the whole answer
Why disabling SCORM standards mode may not restore resume
The SCORM standards-mode setting is real and worth checking. It's a single site setting and instantly reversible, so it costs nothing to confirm. The mistake is treating it as the whole answer and concluding “Moodle is broken” when changing it does nothing.
Say the setting was switched on and you switch it off. Now Moodle will happily store a 10,000-character suspend_data. But if the package's wrapper still clips its output to 4,096 before calling LMSSetValue, Moodle is handed a 4,096-character string and stores that faithfully. The stored value is exactly as short as before. This is not hypothetical: in discussion 470344 the poster reports relaxing the strict SCORM 1.2 data limit and still hitting the problem, because other things were in play (the package's own truncation, single-SCO behaviour, multi-session overwrite).
There's a second-order catch. That setting is a site-wide switch, so changing it touches every SCORM activity on the server, including ones authored by people who quietly depend on the 1.2 limits for correctness. It's not a per-course dial, and on a shared or multi-tenant Moodle you want to know that before you flip it, which is a managed-hosting judgment call rather than a per-activity tweak.
So treat the setting as the first rung, not the ladder. Confirm it's disabled, then re-measure the stored length (the console one-liner is below). If the number moves, the server was your bottleneck and you may be done. If it doesn't budge, and on a default site it usually won't, your bottleneck is upstream in the package, and no amount of Moodle configuration will touch it.
While you're in the activity settings, note the per-activity levers that interact with all of this, because they're the next things you'll reach for. Use the exact labels:
- Auto-commit commits data to the database about 60 seconds after values are set, for packages that never call
LMSCommit()themselves. Turn it on for anything that only saves on a clean exit. - Force new attempt — when set so every entry is a new attempt, a returning learner never resumes; the old attempt's data still exists but isn't what loads. For resume-sensitive courses you don't want this forcing a fresh attempt on re-entry.
- Auto-continue auto-launches the next SCO in multi-SCO packages only; it does nothing for a single-SCO course.
- Grading method — Learning Objects, Highest grade, Average grade, or Sum grade. Learning Objects counts completed/passed SCOs, which is the one you want when the package reports
cmi.core.lesson_statusbut notcmi.core.score.raw. - Mastery score overrides status — if a mastery score is set and the raw score is below it at
LMSFinish, Moodle forceslesson_statusto failed, overriding what the package sent.
The counter-intuitive one
Advance overwriting: an earlier completed section resets to 0%
This is the one that makes people think their LMS is haunted, and it has no tidy name in the docs, so we borrow the one that fits: advance overwriting. The symptom is specific and counter-intuitive. A learner completes an earlier section, moves on, completes a later section, and the earlier one reverts to 0%. Finishing more work destroys work already done. Discussion 470344 on moodle.org is full of people describing exactly this and getting told, unhelpfully, to check their attempt settings.
Often it's truncation wearing a costume. The package keeps its full picture of “sections completed” inside suspend_data. On a later session it resumes by reading the stored value back, but if that stored value was truncated, the package initializes from an incomplete picture missing the early-section completion flags. The learner finishes the current section, the package serializes its now-current state, and Moodle overwrites the row with this fresh snapshot, one that reflects what the package believes right now, which no longer includes the early sections it never managed to read back. The completion flags for sections one and two aren't corrupted so much as forgotten and re-saved as absent.
The tell that separates advance overwriting from plain truncation: with truncation you lose the tail, your newest and furthest progress; with advance overwriting you lose the head, old settled progress, while recent progress sticks. If a learner says “the module I finished last week is back to zero but today's is fine,” that's not a size problem and republishing to 2004 won't touch it. It's an order-of-operations problem between the package's snapshot logic and Moodle's attempt handling.
If your rows changed the day you replaced the package or bumped Moodle versions, that's more likely a data-migration issue; see grades gone after re-enrolment for how to recover those from history. And if the erosion began right after a Moodle upgrade, suspect resume behaviour directly: there is a known Moodle 4.3 SCORM completion regression, MDL-79967, and packages that mix review and normal modes are the usual triggers.
Prove it yourself
Two diagnostics that name the cause in ten minutes
You don't have to reason about this in the abstract. Two measurements, one in the browser and one in the database, tell you exactly where the cut is and which side owns it.
1. Read the live length in the console. Open the SCORM activity, press F12, and in the console's frame picker select the frame holding the actual course content, not the Moodle shell. Because the content runs inside an iframe, the SCORM 1.2 API object usually lives a frame or two up, so walk the window chain to find it, then read the length of what the package currently holds:
// SCORM 1.2 — find the API, then measure the current suspend_data
function findAPI(w){ let n=0; while(w && !w.API && w.parent!==w && n++<10){ w=w.parent; } return w && w.API; }
const api = findAPI(window) || (window.opener && findAPI(window.opener));
if (!api) { console.warn('SCORM API not found — pick the course-content frame in the console frame dropdown'); }
api.LMSGetValue('cmi.suspend_data').length;
// SCORM 2004 uses window.API_1484_11 and api.GetValue('cmi.suspend_data')
Take the course to the point where it breaks and watch this number. If it climbs and then flatlines at exactly 4096, the package is capping itself client-side, gate one, upstream of Moodle. If it climbs well past 4096 in the browser but the stored value (next query) is 4096, Moodle made the cut on write. That single comparison, browser length versus stored length, is the whole diagnosis.
2. Read what actually landed, per learner, per attempt. The database is ground truth. The catch is that Moodle changed these tables in 4.3, so the query depends on your version. Check Site administration → Notifications first, and remember the mdl_ prefix is only the default; yours is whatever $CFG->prefix is set to.
Moodle 4.2 and earlier stored every CMI element in one flat table, scorm_scoes_track:
SELECT t.userid, t.attempt,
CHAR_LENGTH(t.value) AS chars, -- CHAR_LENGTH, not LENGTH: LENGTH counts bytes on MySQL/MariaDB
t.value
FROM mdl_scorm_scoes_track t
WHERE t.scormid = 42 -- mdl_scorm.id, the activity INSTANCE id (not the cmid in view.php?id=)
AND t.element = 'cmi.suspend_data'
ORDER BY t.userid, t.attempt;
Moodle 4.3 (MDL-46279) dropped scorm_scoes_track and normalised it into three tables, scorm_attempt, scorm_element, and scorm_scoes_value, so on a current site you join them (scorm_scoes_value.elementid to scorm_element.id, scorm_scoes_value.attemptid to scorm_attempt.id):
SELECT a.userid, a.attempt,
CHAR_LENGTH(v.value) AS chars,
v.value
FROM mdl_scorm_scoes_value v
JOIN mdl_scorm_element e ON e.id = v.elementid
JOIN mdl_scorm_attempt a ON a.id = v.attemptid
WHERE a.scormid = 42 -- mdl_scorm.id, not the cmid
AND e.element = 'cmi.suspend_data'
ORDER BY a.userid, a.attempt;
Read the chars column. A column full of 4096 across affected learners is a signed confession that something is enforcing the 1.2 limit. Cross it against the console number: browser also 4096 means the package cut it; browser larger than stored means Moodle cut it. Values comfortably under 4096 that still fail to resume send you elsewhere (a missed commit, a new-attempt setting, or a broken package), but in our casework, when learners reset past the halfway mark, that column reads 4096 far more often than not. One migration note: on a site upgraded from 4.1 or 4.2, historical rows move in the background, so if the 4.3+ join comes back empty for a learner you know took the course, check the legacy table before assuming the data is gone.
The fix, reversible first
How to fix it: the reversible-first remediation ladder
Once you know which cut is yours, fix it in order of blast radius. Start with changes you can undo in one click, and only climb to the ones that touch content when the rungs above can't clear it. This ordering matters when there's an audit date on the calendar: you want the fastest safe win first, not the most thorough rebuild.
- Confirm SCORM standards mode is disabled, then re-measure. It ships disabled, so most of the time there's nothing to change here; if an admin enabled it, disable it. Re-run the console and SQL checks. If the stored length grows past 4096 and resume returns, you're done: the server was the bottleneck. If the stored length stays pinned at 4096, the cut is in the package; keep climbing. Remember this is site-wide, so weigh it accordingly on shared servers.
- Set the per-activity levers. Turn on Auto-commit so a closed tab doesn't cost the last stretch. Make sure Force new attempt isn't forcing a fresh attempt on re-entry, so returning learners resume. Confirm the package launches in the current window, not a pop-up the browser can block. These are per-activity and reversible, and none of them cost anything.
- Republish the package, mindful that 2004 is not a clean drop-in. If the cap is inside the package, republishing to SCORM 2004 raises its own
suspend_dataceiling from 4,096 to roughly 64,000 characters, usually a single dropdown in Storyline or Captivate. But Moodle does not fully support SCORM 2004 (sequencing and navigation especially), so test the 2004 build before you commit a cohort to it. You must upload it as a new package (old attempts won't migrate), and plan the swap between cohorts so nobody's mid-course when it lands. - Reduce the state the course stores, or move to xAPI. If even 64,000 is tight (enormous branching, huge question banks), trim what the package remembers (fewer resume-sensitive interactions, “resume to slide” rather than full state) or split one giant SCO into several so no single string hits a wall. For state that's unbounded, xAPI keeps it in a Learning Record Store instead of a size-capped string, sidestepping the ceiling entirely. This is the biggest change and the last resort, because it's the least reversible.
On the OSHA course we wasted the first hour poking at attempt settings before the console number made it obvious the package was the one doing the cutting. After that it went quickly: rung one told us the story (stored length wouldn't move), rung two got Auto-commit and resume behaving, and rung three, republishing the same Storyline source and testing it carefully, cleared it. Total measured suspend_data afterward was about 6,150 characters, resuming cleanly at slide 128. The re-audit passed. Start to fix was one business day, most of it spent confirming rather than guessing.
The mobile path
The Moodle app hardcodes the 4,096 cap
The desktop story and the app story can diverge. Disabling standards mode is honoured in a desktop browser, but it was reported not to apply in the Moodle mobile app, which hardcoded CMI_STRING_4096 in its SCORM 1.2 data model (src/addons/mod/scorm/classes/data-model-12.ts). That's from moodle.org discussion 440176 and tracker MOBILE-4191, reported on app version 4.0.2 back in 2022, old enough that its current resolution status isn't public. So treat it as a known limitation to check rather than a permanent fact, and verify against the app version your learners actually run before you conclude the app is still capping them.
First, your console diagnostic runs in a desktop browser, so it measures the desktop path; a learner resetting only in the app can have a completely intact stored value from their web sessions. Confirm the app path separately, on a real device, before you close the ticket. Second, the app is where a killed session, a backgrounded process, or a spotty connection most often strands the last stretch of progress that never committed, and where two devices syncing attempts that both moved on can let one snapshot overwrite the other: the advance-overwriting pattern, with a network round-trip hiding the moment it happened. Persistent commit and sync failures under real traffic are usually an ops problem rather than a content one, which is hosting and ops territory. If your audience is meaningfully mobile, test mobile explicitly.
Quick answers
Questions people ask about this
I disabled SCORM standards mode and progress still resets. Why?
Because that setting only governs Moodle's server-side cap, and it ships ticked, so unticking it is a real step (Moodle will then store up to 64,000 characters); but it is only one of two caps. If the authoring package's own SCORM wrapper already trimmed cmi.suspend_data in the browser (some do, storing it as value.substring(0, 4096) to stay “1.2 compliant”), the string was shortened before Moodle ever saw it, so the server setting can't restore it. Measure the live length in the console: if it flatlines at 4096 in the browser, the package is the bottleneck and you republish, not reconfigure.
How do I know whether the package or Moodle is doing the truncating?
Compare two numbers. In the browser console, read the length of cmi.suspend_data as the learner progresses. In the database, read CHAR_LENGTH(value) for that learner's cmi.suspend_data row (use CHAR_LENGTH, not LENGTH, since LENGTH counts bytes on MySQL/MariaDB). If the browser value flatlines at 4096, the package cut it client-side. If the browser value climbs past 4096 but the stored value sits at 4096, Moodle cut it on write. That single comparison is the whole diagnosis.
Why does finishing a later section erase an earlier completed one?
That's advance overwriting, and it's usually truncation in disguise. The package keeps its “sections completed” flags inside cmi.suspend_data. If the stored string was truncated, the package resumes from an incomplete picture missing the early flags, then writes a fresh snapshot reflecting only its current view, overwriting the earlier completions as if they never happened. It's discussed on moodle.org at discussion 470344. Fix the underlying truncation and the erosion usually stops; if it started right after a Moodle upgrade, suspect the resume and attempt handling instead.
Does moving to SCORM 2004 actually fix it?
Often, when the cap is inside the package: SCORM 2004 raises cmi.suspend_data from 4,096 to 64,000 characters. But it isn't a clean drop-in: Moodle doesn't fully support SCORM 2004, especially sequencing and navigation, and you upload it as a new package so existing attempts don't carry over. Test the 2004 build before you commit a cohort to it. For unbounded state, xAPI stores progress in a Learning Record Store and removes the fixed ceiling.
My query errors on scorm_scoes_track. Is the table gone?
On Moodle 4.3 and later, yes. MDL-46279 dropped mdl_scorm_scoes_track and split it into three tables, mdl_scorm_attempt, mdl_scorm_element, and mdl_scorm_scoes_value, so you join value to element to attempt instead of reading one flat table. On 4.2 and earlier, query the single scorm_scoes_track table. Check your version at Site administration then Notifications, and remember mdl_ is only the default table prefix.
Learners reset in the Moodle app but not in a browser. Why?
Disabling the setting is honoured in the browser but was reported not to apply in the Moodle mobile app, which hardcoded CMI_STRING_4096 in its SCORM 1.2 data model (moodle.org discussion 440176, tracker MOBILE-4191, on app 4.0.2 in 2022). That report is a few years old and its current status isn't public, so verify against the app version your learners actually run rather than assuming it's still broken, and test the offline-capture-then-sync path on a real device before you close the ticket.
When you'd rather just have it fixed
Everything above is here so you can do it yourself, and plenty of teams do: measure the two lengths, find the cut, and work down the ladder. A lot of them have it fixed by lunch. We publish the method because the method isn't the scarce part; the scarce part is the afternoon you don't have when there's an audit on the calendar and forty-one records that need to be true.
If you've read the raw suspend_data, compared browser length to stored length, tried the reversible rungs, and it's still cutting learners off past the halfway mark, that's exactly what our SCORM tracking repair is for. We find which of the two cuts is yours, fix it at the package, settings, or hosting layer, prove the stored length holds, and confirm resume on both desktop and the app before we call it done. A flat $350, one business day, no retainer. It runs on Moodle, but you don't have to know Moodle; you just need the tracking to be real.
Fix my SCORM tracking Talk to an engineer
Not sure which cut is yours? Send the package or a test login and we'll tell you free, within one business day. Repair is $350 flat only if you want us to finish it, or talk it through first at (615) 396-7139.