You added one private key and now nothing deploys. The log might not even mention it. Here is the 4KB ceiling and the clean way around it.
You add one more environment variable. Usually a long one, like an APNs .p8 key, an FCM private key or a Google service account file. Then every deploy fails with "Your environment variables exceed the 4KB limit".
Sometimes it is worse. The build dies with a bare "exit code 2" and over a hundred functions fail to upload. Nothing in the log mentions the new variable. In another version, the deploy goes through but a push integration silently does nothing.
Netlify Functions run on AWS Lambda. Lambda caps a function's whole environment at 4KB, combined. Every variable counts, not just the new one.
A site with dozens of normal variables can sit just under the line. Then one long value pushes it over. A service account JSON or a .p8 key alone can be over 2KB.
There is a second catch. Team level shared environment variables count against every site on the team. So a key added for one project can break another.
Step 1. Find the long values. List your variables and look at their sizes. Anything over about 300 characters is a candidate to move. Private keys and certificate blobs are the usual offenders.
Step 2. Do not delete shared keys to make room. Other functions, or other sites on the team, may depend on them. Moving the long secret out is safer than pruning blindly.
Step 3. Create a private secrets table. In Supabase, make a table that only the service role can read. With row level security on and no policies, the anon and authenticated roles see nothing:
create table if not exists public.app_secrets (
name text primary key,
value text not null
);
alter table public.app_secrets enable row level security;
revoke all on public.app_secrets from anon, authenticated;
Insert the long secret as a row. Do it in the SQL editor, not in chat or in a committed file.
Step 4. Read and cache it in the function. Keep only short identifiers in env, like the project URL and the service key. The function fetches the long secret once and keeps it in memory:
const { createClient } = require('@supabase/supabase-js');
const sb = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY);
const cache = {};
async function getSecret(name) {
if (cache[name]) return cache[name];
const { data, error } = await sb
.from('app_secrets').select('value').eq('name', name).single();
if (error || !data) throw new Error('Secret missing: ' + name);
cache[name] = data.value;
return data.value;
}
Notice the error check. If the read fails, the function fails loudly instead of sending nothing.
Step 5. Remove the long variable from Netlify and redeploy. Check the team level shared variables too, since they count against this site.
Step 6. Confirm the env really changed. UI and API edits to env have been seen to report success without persisting. From a local folder, link the site and read the value back:
netlify link --id <siteId>
netlify env:get NAME
If a value refuses to stick, use netlify env:import <file> and redeploy.
Another option. Keep the big key in the service that owns it, and give the caller only a small shared secret. The caller then asks that service to do the signed work.
-background suffix for long work.Every entry follows the same pattern: the symptom you see, the real cause, the fix that worked, and the rule that stops it coming back. App Store, Google Play, Capacitor, Netlify, Supabase, Stripe, n8n and more.
Get Field NotesBuilding a lot? The Builder Bundle has Field Notes plus the launch playbook, the prompt book and the Claude Code skills kit. See the bundle.
Every API call in the app fails.
The policy looks right.
The bucket exists.
What broke and how we fixed it, across app stores, hosting, databases and payments.