---
title: "Why does Amplitude intermittently throw \"Invalid apiKey\" or a runQueuedFunctions error?"
canonical_url: https://ampl.webclat.com/qa/amplitude-invalid-apikey-sdk-not-ready
description: "Amplitude throws Invalid apiKey or runQueuedFunctions is not a function intermittently, not always - the cause is almost always init timing, not a wrong key. Here's the fix and how to confirm events are actually landing."
source: Webclat | Amplitude Solutions (official Amplitude partner, independent consultancy)
---

# Why does Amplitude intermittently throw "Invalid apiKey" or a runQueuedFunctions error?

**In short:** An intermittent "Invalid apiKey" or "runQueuedFunctions is not a function" error from Amplitude almost never means the key itself is wrong - it means something called a tracking method before amplitude.init() finished setting up the instance. The fix is to gate every track/identify call behind init's completion (its returned promise in the current Browser SDK, or the queue itself in the legacy snippet) instead of assuming the SDK is ready the instant the script tag runs.

## Why this happens

Amplitude's browser SDK is asynchronous by design: init() kicks off a fetch to Amplitude's servers to resolve configuration and doesn't finish synchronously. If your key were actually wrong, every single call would fail identically, every time. An error that shows up only sometimes - on a slow connection, on first load, on a specific route - is a timing symptom, not a credentials symptom.

The legacy snippet pattern (amplitude-js / SDK 1, loaded via a global amplitude object) works around this with an internal function queue: calls made before init() finishes get queued and replayed once init resolves. "runQueuedFunctions is not a function" specifically means that queue got corrupted or overwritten - usually because the snippet was loaded twice (once from a CDN tag and once from a bundled import), so the second load stomped on the first instance's queue before it could flush.

The current Browser SDK (@amplitude/analytics-browser, SDK 2) replaced the queue with a real Promise from init(), which removes the runQueuedFunctions failure mode entirely - but it introduces its own version of the same bug if you call amplitude.track() immediately after amplitude.init(apiKey) without awaiting or chaining off that promise, especially inside a framework that runs your setup code more than once (React StrictMode's double-invoke, or a route change that re-imports the module).

## Fix it

### Browser SDK 2 (@amplitude/analytics-browser)

1. Call init() exactly once at the top of your app's entry point, and capture the promise it returns instead of calling track() on the next line: `const initPromise = amplitude.init(apiKey, { defaultTracking: true }).promise;`
2. Anywhere you need to guarantee ordering (a page-load event that must be the very first event in a session), await that promise before tracking: `await initPromise; amplitude.track("App Loaded");`. For everything else, amplitude.track() calls made after init() is invoked (even before it resolves) are safe - SDK 2 buffers them correctly, unlike the legacy queue.
3. Search your codebase for more than one `amplitude.init(` call. A second init - often added by a second engineer wiring up a new feature, or by a CDN snippet left in alongside an npm import - resets the instance and is the most common source of this error in practice. Keep exactly one init call, in one file.
4. If you're on a framework with double-invoked effects in development (React 18 StrictMode), guard the init call with a module-level flag or run it outside the component tree entirely (a dedicated analytics.ts singleton imported once), so development-only double-mounting can't trigger it twice.

### Legacy snippet (amplitude-js / SDK 1)

1. Confirm the amplitude snippet is loaded from exactly one place. Grep your HTML templates and your bundled JS for `getInstance().init(` - if it appears in both a `<script>` tag in the page head and inside an imported module, remove one. The second load replaces `window.amplitude`, and the first instance's queue (and its pending `runQueuedFunctions`) goes with it.
2. Move all tracking calls to fire off the documented `amplitude.getInstance().init(apiKey, userId, options, callback)` completion callback (or a short delay after init if no callback is supported in your snippet version) rather than firing them synchronously right after the init line.
3. If you cannot remove a duplicate load (e.g. a tag manager also injects the snippet), wrap every tracking call in a defensive check: `if (window.amplitude && window.amplitude.getInstance) { ... }` so a load-order race degrades to a dropped event instead of a thrown error.
4. Plan a migration to the Browser SDK 2 package: the queue-corruption failure mode this variant describes doesn't exist in the newer SDK's promise-based init, so it is the permanent fix rather than a mitigation.

## How to verify it worked

1. Open your browser's Network tab, filter for `api.amplitude.com` (or `api2.amplitude.com`), and reload the page. You should see exactly one request pattern per tracked event, with no requests carrying an empty or malformed api_key parameter.
2. In Amplitude, open the project's Events explorer (or User Look-Up for a specific test user/device) and confirm the events you just fired appear within a minute or two of sending them - Amplitude's ingestion isn't instant, so give it a short window before concluding something failed.
3. Reproduce the original failure condition deliberately: throttle your connection to "Slow 3G" in devtools and reload. If the error is gone under throttling (the exact condition that used to trigger it), the timing fix held; if it still fires, there's a second init call you haven't found yet.
4. Leave the fix running for a full day of real traffic and check your project's event volume trend for a discontinuity - a sudden drop right after deploying a "fix" usually means events are now being silently swallowed by an overly defensive guard rather than actually delivered.

## FAQ

### Could this actually be a wrong or revoked API key?

It's worth ruling out first since it's the fastest check: copy the exact key from Amplitude's project settings and compare it character-for-character against what's in your code or environment variable, watching for a stray space or a key from the wrong project (test vs. production). But a wrong key fails every single call consistently - if the error only shows up sometimes, a bad key isn't the explanation and you're looking at a timing issue instead.

### Does this affect server-side tracking through the HTTP API or Node SDK too?

No - this specific failure is a browser-SDK initialization race. Server-side sends via the HTTP API V2 or the Node SDK don't have a client-side init sequence to race against; a bad key there returns an explicit error response from Amplitude's API rather than an intermittent client-side exception.

Related: [What's the correct setup sequence to get Amplitude tracking events inside a React, React Native, or Expo app?](https://ampl.webclat.com/qa/integrate-amplitude-react-react-native-expo), [Why doesn't the Amplitude snippet load with Next.js's Script component, or why do events go missing right after a redirect?](https://ampl.webclat.com/qa/amplitude-nextjs-script-tag-missing-events), [Why does Amplitude fail to load with ERR_BLOCKED_BY_CLIENT, and how much of my traffic does that cost me?](https://ampl.webclat.com/qa/amplitude-blocked-by-ad-blockers)
