Using Love2D's love.filesystem for Reliable Cross‑Platform Save Data
Learn how to use Love2D's love.filesystem API to create portable, reliable save files that work on desktop and mobile, with verification steps and recovery strategies.
25 Sept 2026, 23:19 UTC

Desired outcome
Ensure that player progress is stored safely and can be read back on any desktop or mobile platform where the Love2D game runs, without breaking the sandbox guarantees of the engine.
Prerequisites
- Love2D version 11.4 or newer (the
love.filesystemAPI is stable across this range). - A basic Love2D project with a
main.luaentry point. - Familiarity with Lua tables and simple serialization (e.g., using
return { ... }or a lightweight serializer likeserpent). - Access to a terminal or command prompt to launch the game (
love .from the project folder).
Procedure
- Define a save schema
Choose a version number for your save format and decide which game state fields need persistence (e.g., player level, inventory, settings). Example schema:
local SAVE_VERSION = 1 local function defaultSave() return { version = SAVE_VERSION, level = 1, inventory = {}, settings = { fullscreen = false, volume = 0.8 } } end - Serialize the state
Convert the Lua table to a string that can be written to disk. A simple approach is to use
returnsyntax so the file can bedofile‑ed later:local function serializeSave(tbl) return "return " .. serpent.dump(tbl, {nocode=true, indent=' '}) end(If you prefer not to add a dependency, you can implement a minimal table‑to‑string routine for basic types.)
- Write to a temporary file then rename
This two‑step pattern protects against partial writes caused by power loss or crashes:
local function saveGame(saveTbl) local data = serializeSave(saveTbl) local tempPath = "save.tmp" local savePath = "save.dat" -- Write temporary file local success, err = love.filesystem.write(tempPath, data) if not success then return false, "temp write failed: " .. tostring(err) end -- Rename (atomic on most platforms) success, err = love.filesystem.remove(savePath) -- ignore if missing success, err = love.filesystem.rename(tempPath, savePath) if not success then return false, "rename failed: " .. tostring(err) end return true, nil end - Read and validate the save
When loading, check the file exists, deserialize, and verify the version header. If anything is wrong, fall back to defaults.
local function loadGame() local path = "save.dat" if not love.filesystem.getInfo(path) then return defaultSave(), "no save file found" end local contents, size = love.filesystem.read(path) if not contents then return defaultSave(), "read failed" end local chunk, err = love.loads(contents) -- love.loads is available in 11.4; if not chunk then return defaultSave(), "failed to compile save: " .. err end local ok, saved = pcall(chunk) if not ok or type(saved) ~= "table" or saved.version ~= SAVE_VERSION then return defaultSave(), "invalid or outdated save" end return saved, nil end - Handle fused builds
When the game is bundled with
love.exe(Windows) or similar tools, the source directory becomes read‑only. Your save logic does not need to change, but you should guard against accidental writes to the source base:if love.filesystem.isFused() then -- love.filesystem.getSourceBaseDirectory() is read‑only; ignore for saves end
Expected checks
- After a save operation, call
love.filesystem.getInfo("save.dat")and confirm that thesizematches the length of the serialized string. - On load, compare the returned table’s
versionfield withSAVE_VERSION; mismatches trigger the fallback path. - Run the game on each target platform (desktop Windows/macOS/Linux, Android/iOS) and inspect the save directory via
love.filesystem.getSaveDirectory()to verify thatsave.datappears after exiting. - For fused builds, launch the exported executable and ensure that writes still succeed while attempts to write to
love.filesystem.getSourceBaseDirectory()fail (you can catch the error to confirm).
Recovery options
- Rotating backups: keep the last two saves (
save.datandsave.dat.bak). Before writing the new temporary file, rename the existingsave.dattosave.dat.bak. If the new save fails validation, restore the backup. - In‑memory fallback: if any write or read error occurs (e.g.,
ENOSPCon mobile when storage is low), start the game withdefaultSave()and show a dialog informing the user that progress could not be saved. - Diagnostic logging: during development, print
love.filesystem.getSaveDirectory()to the console so you can locate the sandbox folder on each device.
Limitations
- The
love.filesystemAPI is synchronous; large save blocks may cause a noticeable frame hitch. Perform saves during loading screens or split the data into chunks. - On iOS and Android, the operating system may delete the sandbox when the app is offloaded or storage is critically low. Treat local saves as a cache; for critical data consider syncing to a remote service.
- If you need to store binary blobs (e.g., screenshot textures), encode them as base64 strings or write them as separate files using the same temporary‑then‑rename pattern.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.