Every app accumulates data it no longer needs: expired sessions, week-old logs, finished giveaways, cache that's gone cold. Left alone it grows without limit — disk fills, queries slow down, backups bloat. Retention is the deliberate opposite: deciding what has a natural expiry and removing it on purpose. How you do that depends entirely on the store, because some delete old data for you and some don't.
| At a glance | |
|---|---|
| You need | Any database with data that piles up over time |
| Plan | Any |
| Helps to know | SQLite and Caching with Redis |
First, decide what actually expires
Not all data should be cleaned. Ask of each kind: if I deleted this a month from now, would anyone miss it?
- Yes, delete it eventually: sessions, cooldowns, rate-limit counters, logs, temporary uploads, one-off job records, old cache.
- No, keep it: users, purchases, posts, settings, anything you can't recreate.
Retention only ever applies to the first list. When in doubt, keep it — and if you must clean something valuable, archive it somewhere first.
The easy case: stores with built-in expiry
Two of the databases you'll use on Falix delete old data for you if you tell them how.
Redis — expiry is the whole point. Set a lifetime when you write the key and it self-deletes; there's no cleanup job at all:
await client.set(`session:${token}`, userId, { EX: 3600 }); // gone in an hour
That covers sessions, cooldowns, and rate limits with zero maintenance. See Caching with Redis and Redis from Python.
MongoDB — a TTL index makes the database delete documents automatically once a date field is old enough. Create it once:
// delete each doc 24 hours after its createdAt
await db.collection('logs').createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 60 * 60 * 24 }
);
Insert documents with a createdAt date and Mongo sweeps out the expired ones on its own — retention with no code to run.
The manual case: SQL databases
SQLite, MySQL, and PostgreSQL have no automatic expiry. You clean up by deleting old rows yourself — which means two things: store a timestamp, and index it.
1. Give the table a timestamp so "old" is a question you can ask:
CREATE TABLE logs (
id INTEGER PRIMARY KEY,
message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_logs_created ON logs (created_at);
The index matters: without it, every cleanup scans the whole table.
2. Delete past a cutoff:
-- SQLite / MySQL: remove rows older than 30 days
DELETE FROM logs WHERE created_at < DATETIME('now', '-30 days'); -- SQLite
DELETE FROM logs WHERE created_at < NOW() - INTERVAL 30 DAY; -- MySQL
3. Delete in batches for big tables. One giant DELETE can lock the table and run for a long time. Cap it and repeat until nothing's left:
DELETE FROM logs WHERE created_at < DATETIME('now', '-30 days') LIMIT 5000;
⚠️ Heads up: A
DELETEwith a wrong (or missing)WHEREclause empties the table. Always take a database backup — or a server backup for SQLite — before a mass delete, and run the matchingSELECT count(*)first to see exactly how many rows theWHEREwill hit.
Automating the manual case honestly
Here's the honest Falix detail: the panel's Schedules run console commands and power actions, not SQL — so you can't point a schedule directly at a DELETE. Automate SQL cleanup from inside your own long-running app instead:
- A Node app or bot: run the delete on a timer — a daily
setInterval, or anode-cronjob. A bot can usediscord.jsalongside a scheduled task. - A Python app or bot: a background loop,
discord.ext.tasks, or a scheduler library. - Or on startup: a small cleanup query each time the app boots handles low-traffic cases well enough.
The pattern is always the same — the app that owns the data also prunes it, on whatever clock suits.
Retention at a glance
| Store | Built-in expiry? | How you retain |
|---|---|---|
| Redis | Yes | EX / EXPIRE per key — self-deletes |
| MongoDB | Yes | A TTL index (expireAfterSeconds) on a date field |
| SQLite | No | Scheduled DELETE ... WHERE created_at < cutoff from your app |
| MySQL / PostgreSQL | No | Same — timestamped rows, indexed, deleted on a timer |
Soft delete vs hard delete
Sometimes you want data gone from view but not truly destroyed — an "undo" window, or a record you might need for an audit. That's a soft delete: add a deleted_at column, set it instead of deleting the row, and filter it out in your queries (WHERE deleted_at IS NULL). A separate job hard-deletes rows whose deleted_at is old enough. Use it when "deleted" needs to be reversible; use a plain DELETE when it doesn't.
Reclaiming space
Deleting rows frees space inside the database, but the file on disk may not shrink right away:
- SQLite: run
VACUUMoccasionally to rebuild the file at its smaller size (it needs room to do so, and briefly locks the database). - MySQL / PostgreSQL: the engine reuses freed space automatically; you rarely need to intervene on a managed database.
Verify it works
Run your delete, then a SELECT count(*) before and after to confirm the right number of rows went — and only those. For a TTL index or a Redis key, write a record with a short expiry, wait past it, and query again: it should be gone with no action from you. Watch your database size trend down over the following days once a retention job is running.
Troubleshooting
- The delete ran but the file didn't shrink — that's normal; deleted space is reused. Run
VACUUMon SQLite if you specifically need the file smaller. - Cleanup is slow or locks the app — you're missing an index on the timestamp, or deleting too much at once. Add the index and delete in
LIMITbatches. - A TTL index isn't deleting anything — the field must be a real date type, not a string or a number, and the index must set
expireAfterSeconds. - I deleted too much — restore the backup you took first. If you didn't take one, this is why the heads-up above exists.