FCM error codes, and what to do about each
Every FCM HTTP v1 error code: UNREGISTERED, INVALID_ARGUMENT, SENDER_ID_MISMATCH, QUOTA_EXCEEDED and more, with the Admin SDK codes and when to retry.
Every failed FCM HTTP v1 send returns an HTTP status, a status word, and usually an errorCode inside details. The errorCode is the one to act on. This page lists each one, what causes it, whether to retry, and the matching Firebase Admin SDK code, from Google’s own reference.
{
"error": {
"code": 404,
"message": "Requested entity was not found.",
"status": "NOT_FOUND",
"details": [
{ "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "UNREGISTERED" }
]
}
}
Every FCM v1 error code
| errorCode | HTTP | What it means | What to do | Retry |
|---|---|---|---|---|
UNREGISTERED |
404 | The token is dead: app uninstalled, data cleared, app unregistered, or the token refreshed | Delete the token. Never send to it again | No |
INVALID_ARGUMENT |
400 | The request is malformed: a bad token, a non-string data value, a reserved key, a payload over the limit, a bad TTL |
Read message and the fieldViolations in details; fix the field it names |
No |
SENDER_ID_MISMATCH |
403 | The token belongs to another Firebase project than your service account | Send with the project the app registered under | No |
QUOTA_EXCEEDED |
429 | Too many messages: for the project, for one device or for one topic | Slow down; back off from at least 1 minute | Yes, with backoff |
UNAVAILABLE |
503 | FCM is overloaded and could not answer in time | Back off exponentially, honour Retry-After, add jitter |
Yes, with backoff |
INTERNAL |
500 | An unknown error inside FCM | Retry as for UNAVAILABLE; contact Firebase support if it lasts |
Yes, with backoff |
THIRD_PARTY_AUTH_ERROR |
401 | The APNs key or certificate, or the web push key, is invalid or missing | Upload a valid APNs .p8 key in the Firebase project; check Key ID and Team ID |
No |
UNSPECIFIED_ERROR |
varies | No more information | Log the full response and look at message |
No |
INVALID_ARGUMENT: the five usual causes
The same 400 covers several mistakes. The message says which one:
- “The registration token is not a valid FCM registration token”. The token was cut short, has extra characters (quotes, a newline), or is an APNs device token sent to FCM. Store the token exactly as the app received it from
getToken(). - “Invalid value at ‘message.data[0].value’ (TYPE_STRING)”. Every value in
datamust be a string.{"order_id": 88213}fails;{"order_id": "88213"}works. - Message too big. The payload can be at most 4,096 bytes, or 2,048 bytes for messages to a topic, counting keys and values.
- A reserved key in data.
from,gcm, and anything starting withgoogleare used by FCM itself and are refused. - A bad TTL.
android.ttlis a duration in seconds as a string ending ins, like"3600s", from 0 to 2,419,200 (4 weeks).
Errors from Google before FCM
Some failures come from Google’s front door, before FCM reads the message. They have a status but no FCM errorCode:
| status | HTTP | Usual cause |
|---|---|---|
UNAUTHENTICATED |
401 | The OAuth access token is missing, malformed or expired. Tokens from a service account last one hour; make a new one |
PERMISSION_DENIED |
403 | The Firebase Cloud Messaging API is not enabled in the Google Cloud project, or the service account is from another project or lacks permission to send |
NOT_FOUND |
404 | With UNREGISTERED in details: a dead token. Without it: a wrong project id in the URL |
The URL must carry the project id, not the project number or name: https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send.
Firebase Admin SDK error codes
The Admin SDKs for Node, Java, Python and Go turn the same answers into their own codes. The ones you will meet:
| Admin SDK code | Same as | What to do |
|---|---|---|
messaging/registration-token-not-registered |
UNREGISTERED |
Delete the token |
messaging/invalid-registration-token |
INVALID_ARGUMENT (token) |
Check how the token is stored; do not trim or add characters |
messaging/invalid-argument |
INVALID_ARGUMENT |
Read the message |
messaging/invalid-data-payload-key |
INVALID_ARGUMENT (reserved key) |
Rename the key |
messaging/payload-size-limit-exceeded |
INVALID_ARGUMENT (size) |
Make the payload smaller than 4,096 bytes (2,048 to a topic) |
messaging/mismatched-credential |
SENDER_ID_MISMATCH |
Use the service account of the app’s Firebase project |
messaging/message-rate-exceeded, device-message-rate-exceeded, topics-message-rate-exceeded |
QUOTA_EXCEEDED |
Send less to that target; do not retry at once |
messaging/invalid-apns-credentials |
THIRD_PARTY_AUTH_ERROR |
Upload a valid APNs key or certificate |
messaging/authentication-error |
UNAUTHENTICATED |
Check the credential the SDK starts with |
messaging/server-unavailable |
UNAVAILABLE |
Retry with backoff |
messaging/internal-error |
INTERNAL |
Retry with backoff |
messaging/too-many-topics |
The token is already in the maximum number of topics |
With sendEachForMulticast the call itself succeeds and each token gets its own result. Check response.responses[i].error?.code for every token, not only failureCount:
const res = await getMessaging().sendEachForMulticast({ tokens, notification: { title, body } });
const dead = [];
res.responses.forEach((r, i) => {
const code = r.error?.code;
if (code === "messaging/registration-token-not-registered" || code === "messaging/invalid-registration-token") dead.push(tokens[i]);
});
await db.tokens.deleteMany({ token: { $in: dead } }); // your own store
A retry rule that holds up
- 2xx: done.
- 404
UNREGISTERED, 400 with an invalid token, 403SENDER_ID_MISMATCH: the token will never work. Remove it. - Any other 400 or 401: your request or credentials are wrong. Retrying sends the same mistake; log it and fix it.
- 429, 500, 503: wait and retry. Start at the
Retry-Aftervalue if there is one, else 1 second (1 minute forQUOTA_EXCEEDED), double each time, add a little randomness, and give up after a few tries.
Senders that retry hard without backoff can be blocked by Google, so step 4 matters at scale.
When the send succeeds and nothing shows
A 200 with a message name only means FCM accepted the message. If the phone stays silent, the problem is on the device: a data-only message to a closed app, a missing notification permission, Doze, or a phone maker’s battery saver. See FCM says success but the notification never arrives and push on Xiaomi, Oppo, Vivo and Realme.
What SignOps does with these
SignOps Push sends through FCM HTTP v1 with your own service account and reads each answer the same way:
UNREGISTERED,SENDER_ID_MISMATCHand an invalid-tokenINVALID_ARGUMENTremove the device and postdevice.removedto your webhook.- 429 and 5xx answers are retried.
- Anything else is recorded as the push’s failure reason, which the dashboard shows under See what Google said.
For the iOS side, see APNs error codes.
Sources: Firebase: FCM error codes, Firebase: ErrorCode reference for HTTP v1, Firebase: FCM throttling and quotas.