Section 2 · AlarmManager

The full alarm API — clock types, exactness levels, Doze behaviour, and when to use which variant


What AlarmManager Is

AlarmManager is the Android system service for scheduling work at a specific point in time or after a specific interval. It operates at the system level: the alarm fires even if the app is not running and even if the device was restarted (provided the alarm is re-registered after boot via BOOT_COMPLETED).

AlarmManager is distinct from WorkManager and JobScheduler:

  AlarmManager WorkManager JobScheduler
Timing precision Exact (with permission) or approximate Approximate (constraint-based) Approximate
Survives app kill Yes — system-level Yes — persisted to disk Yes — registered with system
Survives reboot No — must re-register after BOOT_COMPLETED Yes — automatic No — must re-register
Battery impact High when using exact alarms Low — batched Low — batched
Doze-aware setExactAndAllowWhileIdle can fire during Doze Deferred to maintenance window Deferred to maintenance window
Use case Calendar reminders, medication alarms, exact-time notifications Deferrable background sync Deferrable background work

Android Developers — Schedule alarms


Clock Types

Every alarm method takes a type parameter that controls two things: whether the time reference is real-world clock time or elapsed time since boot, and whether the alarm wakes the device from sleep.

Type constant Time reference Wakes device? Use when
RTC Real-world time (UTC milliseconds) No Fire at wall-clock time, device must already be awake
RTC_WAKEUP Real-world time (UTC milliseconds) Yes Fire at wall-clock time, must fire even if screen is off
ELAPSED_REALTIME Milliseconds since boot No Fire after a duration, device must already be awake
ELAPSED_REALTIME_WAKEUP Milliseconds since boot Yes Fire after a duration, must fire even if screen is off

For user-facing alarms (reminders, medication timers): use RTC_WAKEUP.

For interval-based background work (poll every 15 minutes): use ELAPSED_REALTIME_WAKEUP.

1
2
3
4
5
6
7
8
9
val alarmManager = getSystemService(AlarmManager::class.java)

// Fire at a specific wall-clock time, waking the device if needed
val triggerAt = System.currentTimeMillis() + 30 * 60 * 1000L   // 30 minutes from now
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent)

// Fire after 1 hour of elapsed boot time, waking the device
val triggerElapsed = SystemClock.elapsedRealtime() + 60 * 60 * 1000L
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerElapsed, pendingIntent)

System.currentTimeMillis() gives wall-clock UTC time. SystemClock.elapsedRealtime() gives monotonic time since boot. The monotonic clock is immune to user time changes and NTP adjustments, making it better for interval-based alarms.


Exactness Levels

Android offers several alarm methods with different precision guarantees and Doze behaviour:

set() — inexact, batch-aligned

The alarm fires at approximately the requested time. The system may defer it by up to 75% of the interval to batch alarms from multiple apps together. In Doze, it is deferred to the next maintenance window.

1
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent)

Use for work where approximate timing is acceptable (refresh a widget, pre-fetch content).

setInexactRepeating() — repeating, batch-aligned

Repeating alarm with inexact timing. The system aligns repetitions from multiple apps together where possible to reduce wake-ups. The minimum interval is AlarmManager.INTERVAL_FIFTEEN_MINUTES.

1
2
3
4
5
6
alarmManager.setInexactRepeating(
    AlarmManager.ELAPSED_REALTIME_WAKEUP,
    SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_FIFTEEN_MINUTES,
    AlarmManager.INTERVAL_HOUR,
    pendingIntent
)

Predefined interval constants:

Constant Value
INTERVAL_FIFTEEN_MINUTES 15 minutes
INTERVAL_HALF_HOUR 30 minutes
INTERVAL_HOUR 1 hour
INTERVAL_HALF_DAY 12 hours
INTERVAL_DAY 24 hours

Prefer WorkManager over setInexactRepeating for any periodic work that does not need a notification at a specific time. WorkManager survives reboots automatically and respects constraints.

setExact() — exact, no Doze exemption

Fires at exactly the requested time when the device is awake. In Doze, it is deferred to the next maintenance window — so it may fire late if the device is idle.

1
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent)

Use for alarms that are exact when the device is active, but where firing slightly late during idle periods is acceptable.

setWindow() — exact window, batch-optimised

Fires within a time window you define. This allows the system to align the alarm within the window with other alarms, reducing the number of wake-ups, while still guaranteeing delivery before the window ends.

1
2
3
4
5
6
alarmManager.setWindow(
    AlarmManager.RTC_WAKEUP,
    triggerAt,                      // window start
    15 * 60 * 1000L,                // window length (15 minutes)
    pendingIntent
)

This is the right trade-off for alarms that should be “approximately on time” without batching unpredictably.

setExactAndAllowWhileIdle() — exact, fires during Doze

The most powerful commonly available alarm. Fires exactly on time even if the device is in Doze — it wakes the device from idle state. However, the system limits how often an app can use this: one alarm per app per 9-minute window in deep Doze.

1
2
3
4
5
alarmManager.setExactAndAllowWhileIdle(
    AlarmManager.RTC_WAKEUP,
    triggerAt,
    pendingIntent
)

Use for alarms that must fire at a specific time regardless of Doze — medication reminders, time-sensitive alerts.

setAlarmClock() — calendar alarm, highest priority

The strongest alarm available without special permissions. It fires exactly on time, wakes the device from Doze, and is fully exempt from all Doze restrictions. The system also shows the user an alarm indicator in the status bar. Designed specifically for clock/calendar apps.

1
2
3
4
5
6
val alarmClockInfo = AlarmManager.AlarmClockInfo(
    triggerAt,         // exact fire time in wall-clock milliseconds
    pendingIntent      // PendingIntent shown when user taps the status bar alarm icon
)

alarmManager.setAlarmClock(alarmClockInfo, pendingIntent)

Summary of all alarm methods:

Method Exact? Fires in Doze? Survives Doze? Requires permission?
set() No No No No
setInexactRepeating() No No No No
setExact() Yes No Deferred No
setWindow() Window No Deferred No
setExactAndAllowWhileIdle() Yes Partial (one per 9 min) Yes, rate-limited No
setAlarmClock() Yes Yes Yes No
setExact() + SCHEDULE_EXACT_ALARM Yes No No Yes (API 31+)

Exact Alarm Permissions (Android 12+, API 31)

Before Android 12, setExact() was unrestricted. From API 31 onwards, apps that target API 31+ must hold a permission to use setExact() without Doze deferral:

1
2
3
<!-- AndroidManifest.xml -->
<!-- Request the permission — user must grant it in Special App Access settings -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

Or, for apps where exact alarms are a core use case (clock apps, calendar apps):

1
2
<!-- Automatically granted on install for pre-approved categories -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
Permission Who gets it User can revoke?
SCHEDULE_EXACT_ALARM Any app — user must grant in Settings Yes
USE_EXACT_ALARM Clock, calendar, alarm apps (Play policy-gated) No

Always check before scheduling an exact alarm:

1
2
3
4
5
6
7
8
9
10
11
12
13
val alarmManager = getSystemService(AlarmManager::class.java)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    if (alarmManager.canScheduleExactAlarms()) {
        alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pi)
    } else {
        // Fall back to inexact, or direct the user to grant the permission
        val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
        startActivity(intent)
    }
} else {
    alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pi)
}

The system broadcasts ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED when the user grants or revokes the permission — register a BroadcastReceiver for it if you need to re-schedule alarms reactively.


PendingIntent for Alarms

AlarmManager delivers alarms by firing a PendingIntent. The three common targets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. BroadcastReceiver — most common, lowest overhead
val pi = PendingIntent.getBroadcast(
    context,
    requestCode,
    Intent(context, AlarmReceiver::class.java),
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

// 2. Service — if you need to start a service immediately on alarm fire
val pi = PendingIntent.getService(
    context,
    requestCode,
    Intent(context, UploadService::class.java),
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

// 3. Activity — brings an Activity to the foreground (user-visible alarms)
val pi = PendingIntent.getActivity(
    context,
    requestCode,
    Intent(context, ReminderActivity::class.java),
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

FLAG_IMMUTABLE is required on Android 12+ for alarms that do not need the system to fill in extras. Use FLAG_MUTABLE only when the system must modify the intent (e.g., setAlarmClock’s show intent).


Cancelling Alarms

Cancelling requires an identical PendingIntent — same action, same extras, same request code:

1
2
3
4
5
6
7
8
9
10
11
val cancelIntent = PendingIntent.getBroadcast(
    context,
    requestCode,                // must match the ID used when scheduling
    Intent(context, AlarmReceiver::class.java),
    PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
)

if (cancelIntent != null) {
    alarmManager.cancel(cancelIntent)
    cancelIntent.cancel()       // also cancel the PendingIntent itself
}

FLAG_NO_CREATE returns null if no matching PendingIntent exists — safe to check before cancelling.


Surviving Reboots

AlarmManager alarms are stored in RAM and lost on device reboot. To restore them, listen for BOOT_COMPLETED:

1
2
3
4
5
6
7
8
9
10
class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
        // Re-read all pending alarms from your local database and re-schedule them
        val pending = AlarmRepository(context).getPendingAlarms()
        pending.forEach { alarm ->
            AlarmScheduler(context).schedule(alarm)
        }
    }
}
1
2
3
4
5
6
<receiver android:name=".BootReceiver" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

The canonical pattern: store alarm data in a local database (Room), read it on boot, re-register each alarm with AlarmManager.


Full Example: Medication Reminder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class MedicationAlarmScheduler(private val context: Context) {

    private val alarmManager = context.getSystemService(AlarmManager::class.java)

    fun schedule(medicationId: Long, timeMillis: Long) {
        val intent = Intent(context, MedicationAlarmReceiver::class.java).apply {
            putExtra("medication_id", medicationId)
        }
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            medicationId.toInt(),
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
            !alarmManager.canScheduleExactAlarms()
        ) {
            // Request permission then try again
            context.startActivity(
                Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
            )
            return
        }

        alarmManager.setExactAndAllowWhileIdle(
            AlarmManager.RTC_WAKEUP,
            timeMillis,
            pendingIntent
        )
    }

    fun cancel(medicationId: Long) {
        val intent = Intent(context, MedicationAlarmReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            medicationId.toInt(),
            intent,
            PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
        ) ?: return
        alarmManager.cancel(pendingIntent)
        pendingIntent.cancel()
    }
}

class MedicationAlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val id = intent.getLongExtra("medication_id", -1L)
        if (id == -1L) return

        val notification = NotificationCompat.Builder(context, MEDICATION_CHANNEL)
            .setSmallIcon(R.drawable.ic_pill)
            .setContentTitle("Time to take your medication")
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true)
            .build()

        NotificationManagerCompat.from(context)
            .notify(id.toInt(), notification)
    }
}

AlarmManager vs WorkManager — Decision Guide

Use this When
setAlarmClock() Clock app, calendar alarm — must fire at exact time, Doze-exempt, visible status bar indicator
setExactAndAllowWhileIdle() Medication reminder, time-critical notification — exact, fires during Doze
setExact() Alarm that must be exact when device is awake, tolerate Doze deferral
setWindow() Alarm that should be within a time window, OK if OS picks the best moment
set() Approximate alarm, OK to batch with other apps
WorkManager Background work that must survive reboots, complex constraint logic, retry, chaining

Previous: Doze, App Standby & Battery · ↑ Chapter Index · Next: Firebase Cloud Messaging →


This site uses Just the Docs, a documentation theme for Jekyll.