These three hooks get confused constantly, and the confusion is usually what causes a plugin to leave behind database tables, options, or scheduled cron events long after someone thinks they’ve fully removed it.
Activation: runs once, when the plugin turns on
register_activation_hook(__FILE__, function() {
global $wpdb;
$table = $wpdb->prefix . 'myplugin_logs';
$charset = $wpdb->get_charset_collate();
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta("CREATE TABLE $table (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
message TEXT,
created_at DATETIME
) $charset;");
if (!wp_next_scheduled('myplugin_daily_cleanup')) {
wp_schedule_event(time(), 'daily', 'myplugin_daily_cleanup');
}
});
This is the right place to create tables and schedule recurring events — things that need to exist while the plugin is active.
Deactivation: runs when it’s turned off, but not removed
register_deactivation_hook(__FILE__, function() {
wp_clear_scheduled_hook('myplugin_daily_cleanup');
});
This is the part people skip most often. If you scheduled a cron event on activation but don’t clear it on deactivation, it keeps firing even after the plugin is switched off, because WP-Cron doesn’t know or care whether the plugin is active — it just knows an event was scheduled.
Uninstall: runs only when deleted, not deactivated
// uninstall.php — must be this exact filename, in the plugin's root folder
if (!defined('WP_UNINSTALL_PLUGIN')) exit;
global $wpdb;
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}myplugin_logs");
delete_option('myplugin_settings');
This only runs when someone clicks “Delete” in the plugins screen, never on simple deactivation — which is intentional, since a lot of users deactivate a plugin temporarily without wanting to lose all its data. The check at the top confirms WordPress itself triggered the file, not a direct request to it.
The pattern that avoids most support tickets
Create resources on activation, pause anything recurring on deactivation, and only permanently delete data in uninstall.php. Mixing these up is exactly why “I deactivated the plugin and my cron job is still running” or “I uninstalled it and my custom table is still there” are both such common complaints.