An event is a scheduled job that runs inside MySQL. It is cron, without cron — the server keeps the schedule and executes the SQL itself.
Check the scheduler first
SELECT @@event_scheduler AS event_scheduler;+-----------------+
| event_scheduler |
+-----------------+
| ON |
+-----------------+It is ON by default in MySQL 8. A great deal of advice says otherwise — "your
event never ran because the scheduler is off" — and that was true of MySQL 5.x, where the default
was OFF. Check rather than assume, in both directions: a managed database may well
have turned it off again.
SET GLOBAL event_scheduler = ON; -- until restart
SET PERSIST event_scheduler = ON; -- and afterwards (MySQL 8)With it OFF, events are stored and simply never fire. No error, no warning.
A recurring event
The pizza schema keeps server-side carts, which accumulate: someone builds an order, wanders off, and the rows stay forever. A nightly clean-up:
CREATE EVENT ev_purge_abandoned_carts
ON SCHEDULE EVERY 1 DAY
STARTS '2026-01-01 03:00:00'
ON COMPLETION PRESERVE
COMMENT 'Delete carts untouched for 30 days'
DO
DELETE FROM cart WHERE updated_at < NOW() - INTERVAL 30 DAY;ON COMPLETION PRESERVE matters more than it looks. The default is
NOT PRESERVE, which drops the event once it can no longer run — fine
for a one-off, and for a recurring event with an ENDS clause it means the definition
disappears when it finishes.
STARTS pins the first run. Without it, "every 1 day" means every 24 hours counted
from whenever you created it, so the job drifts to whatever time you happened to deploy.
A one-off
CREATE EVENT ev_one_off_cleanup
ON SCHEDULE AT '2026-03-01 02:00:00'
DO
DELETE FROM order_status_audit WHERE changed_at < '2025-01-01';Useful for scheduling a heavy migration for a quiet hour. It deletes itself afterwards, which for a one-off is what you want.
Several statements
DELIMITER $$
CREATE EVENT ev_nightly_rollup
ON SCHEDULE EVERY 1 DAY
STARTS '2026-01-01 04:00:00'
ON COMPLETION PRESERVE
DO
BEGIN
DELETE FROM cart WHERE updated_at < NOW() - INTERVAL 30 DAY;
DELETE FROM order_status_audit WHERE changed_at < NOW() - INTERVAL 1 YEAR;
END$$
DELIMITER ;Same DELIMITER requirement as a
stored procedure, for the same reason. A neater pattern is
to put the work in a procedure and have the event just CALL it — the logic then lives
somewhere you can test and run by hand.
Managing them
SHOW EVENTS;
SELECT EVENT_NAME, STATUS, INTERVAL_VALUE, INTERVAL_FIELD, LAST_EXECUTED
FROM information_schema.EVENTS WHERE EVENT_SCHEMA = 'pizza';
ALTER EVENT ev_purge_abandoned_carts DISABLE;
ALTER EVENT ev_purge_abandoned_carts ENABLE;
DROP EVENT IF EXISTS ev_purge_abandoned_carts;LAST_EXECUTED is the field to check when someone asks whether the job is running.
DISABLE is better than dropping when you want it back.
Where the errors go
This is the operational weak point. An event that fails writes to the server error log and nowhere else — nothing tells your application, your monitoring, or you. A nightly clean-up can fail every night for a month in complete silence.
If you use events, have the job record its own outcome:
CREATE TABLE job_run (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
job_name VARCHAR(60) NOT NULL,
ran_at DATETIME(6) NOT NULL,
rows_affected INT
);Then alert on the absence of a recent row. "The job did not run" is otherwise invisible, and it is the failure that actually happens.
On a replica
Events do not execute on a replica — the scheduler is disabled there, and the
work arrives through replication instead, which is correct. But it means an event's status is
SLAVESIDE_DISABLED on the replica, and after a failover you must remember to enable the
scheduler on the promoted server. Otherwise every scheduled job silently stops, and nothing reports
it. See replication.
Should this be an event?
| An event is reasonable | Use your scheduler instead |
|---|---|
| Pure SQL clean-up, close to the data | The job calls an API or writes a file |
| You have no other scheduler | You already run cron, Kubernetes CronJobs or Quartz |
| The work must follow the database on failover | You need alerting, retries and logs |
The honest summary: events are convenient and operationally weak. Most teams already have a scheduler with logging, alerting, retries and version control, and a job defined there calling a stored procedure gets the best of both — the SQL stays next to the data, and the scheduling stays somewhere you can see it.
The one thing an event genuinely gives you is that it travels with the database. If the database is the only piece of infrastructure you can rely on, that counts for something.
What to remember
event_scheduleris ON by default in MySQL 8, unlike 5.x. Check it.ON COMPLETION PRESERVE, or the definition disappears when it finishes.STARTS, or the schedule drifts to whenever you deployed.- Errors go to the server error log only. Record your own outcomes and alert on absence.
- Disabled on replicas — re-enable the scheduler after a failover.