<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: YNVAR</title>
    <description>The latest articles on DEV Community by YNVAR (@ynvar_com).</description>
    <link>https://dev.to/ynvar_com</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4004276%2F73fe6aa6-088f-454c-87c6-0432fed4b906.jpg</url>
      <title>DEV Community: YNVAR</title>
      <link>https://dev.to/ynvar_com</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ynvar_com"/>
    <language>en</language>
    <item>
      <title>What to restore when LiteLLM holds team keys, budgets and spend data</title>
      <dc:creator>YNVAR</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:35:22 +0000</pubDate>
      <link>https://dev.to/ynvar/what-to-restore-when-litellm-holds-team-keys-budgets-and-spend-data-4fac</link>
      <guid>https://dev.to/ynvar/what-to-restore-when-litellm-holds-team-keys-budgets-and-spend-data-4fac</guid>
      <description>&lt;p&gt;LiteLLM forwards requests, but a team deployment is not stateless. PostgreSQL holds virtual-key records, teams, budgets and spend data. Stored credentials also depend on the key used to encrypt them.&lt;/p&gt;

&lt;p&gt;That makes the database, configuration and decryption key part of one recovery plan. Restoring only the container or only the database is not enough. This article explains what must be restored, what can be rebuilt, and how the salt key changes master-key rotation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The state a LiteLLM gateway holds
&lt;/h3&gt;

&lt;p&gt;Behind the OpenAI-compatible endpoint, the gateway keeps its state in a PostgreSQL database. The database stores hashed virtual-key records, their scopes, users and teams, budgets, rate limits and spend data. Provider credentials are a separate case: they may be stored in the database encrypted, or supplied from an external secret store, in which case the database never holds them. The gateway also depends on the configuration that defines the model map, and on the key that decrypts any credentials the database does hold.&lt;/p&gt;

&lt;p&gt;If you lose the database, you lose the virtual-key records and the spend history. Provider credentials you keep in an external secret store can be re-injected. So the operational unit is the database plus the configuration plus the decryption key, not the container that serves traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a consistent restore has to bring back
&lt;/h3&gt;

&lt;p&gt;A consistent restore is three things brought back together: the PostgreSQL database, the configuration that points at it, and the key that decrypts stored credentials — the salt key if you set one, or the master key if you did not. The database on its own will not start a working gateway. Version the database and the configuration together as one restore set. The decryption key has to be available to that restore too, but keep it in a separate secret store rather than inside the backup, so the encrypted database and the key that opens it are not sitting in the same place.&lt;/p&gt;

&lt;p&gt;Then rehearse the restore on a throwaway instance. Bring the config and database back, start the gateway on the same pinned version, log into the admin UI, confirm a virtual key still resolves and routes a request, and confirm a model whose credentials live in the database still works.&lt;/p&gt;

&lt;p&gt;A backup you have never restored has not been shown to work. Set backup frequency and retention from how much spend history and how many live keys you could accept losing between backups, and treat that restore rehearsal as the launch gate, not an afterthought.&lt;/p&gt;

&lt;h3&gt;
  
  
  The PostgreSQL database is the system of record
&lt;/h3&gt;

&lt;p&gt;LiteLLM documents PostgreSQL through &lt;code&gt;DATABASE_URL&lt;/code&gt; as a requirement for virtual keys, budgets and spend tracking. That is the documented position, and it is the reason the database is the system of record here.&lt;/p&gt;

&lt;p&gt;The rest of this section is general database operations practice rather than a documented requirement for running the proxy. Once the database holds your keys, users, teams and usage, treat it as production data: give it persistent storage, size the connection pool against the total connection budget your instances and workers share rather than a per-worker guess, and apply the backup-and-restore discipline above.&lt;/p&gt;

&lt;p&gt;Spend logging writes on the request path, so watch that write volume as traffic grows — the production docs give concrete settings for batching those writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What LiteLLM stores in PostgreSQL
&lt;/h3&gt;

&lt;p&gt;The reason to run the gateway instead of sharing one provider key is virtual keys, and they are the state you are protecting.&lt;/p&gt;

&lt;p&gt;Instead of handing an app your real provider key, you issue a gateway key through &lt;code&gt;/key/generate&lt;/code&gt;, scoped to an app or team with a &lt;code&gt;team_id&lt;/code&gt; or &lt;code&gt;user_id&lt;/code&gt; and the models it may call. The real provider keys stay in the gateway; the app only holds a key you can revoke or rotate without touching the provider account.&lt;/p&gt;

&lt;p&gt;A key request is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"team_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"team-web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"models"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-3-5-sonnet"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_budget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"budget_duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rpm_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;team_id&lt;/code&gt; has to reference a team you have already created, and you get back an &lt;code&gt;sk-&lt;/code&gt; virtual key the app uses as its API key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;max_budget&lt;/code&gt; and &lt;code&gt;budget_duration&lt;/code&gt; set a spend cap and its reset window; &lt;code&gt;rpm_limit&lt;/code&gt;, &lt;code&gt;tpm_limit&lt;/code&gt; and &lt;code&gt;max_parallel_requests&lt;/code&gt; cap request rate, token rate and concurrency, so one runaway job cannot spend the month in an afternoon.&lt;/p&gt;

&lt;p&gt;These apply only where you configure them and enforce against the gateway's own accounting, not the provider invoice. How a budget resets and what happens at the limit are version-specific — confirm the field names and behaviour against the docs for the version you run, because the key API changes between releases.&lt;/p&gt;

&lt;p&gt;All of this is stored in PostgreSQL, which is why the restore above matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the salt key changes master-key rotation
&lt;/h3&gt;

&lt;p&gt;Two secrets protect that state, and they behave differently.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LITELLM_MASTER_KEY&lt;/code&gt; is the proxy's admin credential — it must start with &lt;code&gt;sk-&lt;/code&gt;, and it creates and manages every virtual key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LITELLM_SALT_KEY&lt;/code&gt;, when you set it, is the key that encrypts stored credentials — models, the config &lt;code&gt;environment_variables&lt;/code&gt;, and the credentials table — in the database.&lt;/p&gt;

&lt;p&gt;Set the salt key from the start, because the master-key rotation procedure splits on exactly whether it is present, and the official rotation guide is worth reading before you rotate anything.&lt;/p&gt;

&lt;p&gt;If the master key leaks, an attacker has proxy-admin control: they can create, revoke and re-scope virtual keys, change the configuration, and use the model access the gateway is configured with. Where no salt key is set, the master key is also the credential-encryption key, so they can decrypt the stored credentials too.&lt;/p&gt;

&lt;p&gt;Back up PostgreSQL before either rotation, whichever case you are in.&lt;/p&gt;

&lt;p&gt;With a salt key configured, the master key is only an authentication credential. To rotate it you generate a new value, update whichever master-key source your deployment uses — the &lt;code&gt;LITELLM_MASTER_KEY&lt;/code&gt; environment variable or &lt;code&gt;general_settings.master_key&lt;/code&gt; in the config — so every instance gets the same new value, then restart them all.&lt;/p&gt;

&lt;p&gt;You do not call &lt;code&gt;POST /key/regenerate&lt;/code&gt; with &lt;code&gt;new_master_key&lt;/code&gt; here — the docs warn that doing so would re-encrypt your stored credentials under a key the proxy never uses to decrypt, which strands them.&lt;/p&gt;

&lt;p&gt;Without a salt key, the master key also encrypts those stored credentials, so rotating it has to re-encrypt them. That is why the database backup matters especially here: the re-encryption deletes and recreates rows and skips any row it cannot re-encrypt, so a partial failure can strand data.&lt;/p&gt;

&lt;p&gt;Call &lt;code&gt;POST /key/regenerate&lt;/code&gt; with the current master key as &lt;code&gt;key&lt;/code&gt; and the new one as &lt;code&gt;new_master_key&lt;/code&gt;. It re-encrypts the stored models, the config environment variables, and the credentials, MCP-server and user tables.&lt;/p&gt;

&lt;p&gt;After it returns, update the same master-key source — the &lt;code&gt;LITELLM_MASTER_KEY&lt;/code&gt; environment variable or &lt;code&gt;general_settings.master_key&lt;/code&gt; — so every instance gets the new value, restart them all, log into the admin UI with the new key, and test a request against a DB-stored model.&lt;/p&gt;

&lt;p&gt;Neither of those is the same as rotating a single virtual key, which is a different operation on a different endpoint, &lt;code&gt;POST /key/{key}/regenerate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Virtual keys are stored hashed rather than encrypted, so they keep working through a master-key rotation. You regenerate one only when that specific key is compromised.&lt;/p&gt;

&lt;p&gt;The documented rotation flows assume that you still hold the current master key. Keep it in a real secret store, keep the salt key separate from it, and rehearse a rotation before you need one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis is not part of the restore
&lt;/h3&gt;

&lt;p&gt;You do not need Redis for a single instance, and when you do use it, it is not part of what you restore.&lt;/p&gt;

&lt;p&gt;It becomes relevant once you run more than one proxy, where it shares rate-limit counters and router state across instances so limits apply across the fleet rather than per process.&lt;/p&gt;

&lt;p&gt;It backs the response cache when you have enabled caching and pointed it at Redis, and it can cache virtual-key lookups when you set &lt;code&gt;enable_redis_auth_cache&lt;/code&gt;, which keeps the database off the path of every request.&lt;/p&gt;

&lt;p&gt;PostgreSQL is the durable system-of-record state. Redis holds rebuildable cache, counter and router state, so losing it costs cache warmth and cross-instance counters, not your virtual-key records or spend history.&lt;/p&gt;

&lt;p&gt;Restore PostgreSQL and allow the Redis cache and distributed counters to rebuild.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrades change the schema you are restoring
&lt;/h3&gt;

&lt;p&gt;The schema of that PostgreSQL database moves with LiteLLM, which matters because it is the thing you back up and restore.&lt;/p&gt;

&lt;p&gt;The project ships Prisma migrations gated by &lt;code&gt;USE_PRISMA_MIGRATE&lt;/code&gt; and &lt;code&gt;DISABLE_SCHEMA_UPDATE&lt;/code&gt;, versioned so older releases keep using their own migrations.&lt;/p&gt;

&lt;p&gt;Treat an upgrade as a change to production data: pin the image to a specific tag instead of tracking &lt;code&gt;latest&lt;/code&gt;, take a database backup first, and read the release notes.&lt;/p&gt;

&lt;p&gt;In a multi-pod deployment, run migrations from one place and set &lt;code&gt;DISABLE_SCHEMA_UPDATE&lt;/code&gt; on the rest so pods do not race the same migration.&lt;/p&gt;

&lt;p&gt;A restore is only clean if you bring the database back on a version whose schema matches it, which is another reason the pinned version is part of the restore rehearsal above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the admin UI off the open internet
&lt;/h3&gt;

&lt;p&gt;The admin UI at &lt;code&gt;/ui&lt;/code&gt; is an access path to all of that state — it creates keys, adds models and shows spend.&lt;/p&gt;

&lt;p&gt;It has its own login through &lt;code&gt;UI_USERNAME&lt;/code&gt; and &lt;code&gt;UI_PASSWORD&lt;/code&gt; or through SSO, and you can turn it off entirely with &lt;code&gt;DISABLE_ADMIN_UI="True"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The docs do not say it must never be public; that is a call I make anyway.&lt;/p&gt;

&lt;p&gt;Keep the admin surface behind its login and, where you can, off the public internet, with the gateway behind a reverse proxy over TLS rather than on a raw published port. It serves traffic fine without the UI being publicly reachable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure modes around the state
&lt;/h3&gt;

&lt;p&gt;The failures worth designing around are all about that state.&lt;/p&gt;

&lt;p&gt;A leaked master key exposes proxy-admin control, and the stored credentials too if no salt key is set.&lt;/p&gt;

&lt;p&gt;A database you cannot restore costs you the keys and the spend history, which is why the restore rehearsal is the gate.&lt;/p&gt;

&lt;p&gt;One failure shows up in the spend the database records rather than in the storage itself: fallback between providers can raise spend sharply if a cheap model fails over to an expensive one, depending on how retries and billing are configured. Cap it with the per-key budgets you already keep in that database and watch the spend when it triggers.&lt;/p&gt;

&lt;p&gt;A public, unauthenticated admin UI is a direct path to the key namespace.&lt;/p&gt;

&lt;h3&gt;
  
  
  When you do not need this
&lt;/h3&gt;

&lt;p&gt;If one script uses one provider key, none of this applies — you would be adding a stateful service, a database and an admin surface to operate for no gain over calling the provider directly.&lt;/p&gt;

&lt;p&gt;The restore question only becomes real when several apps, teams or people share model access through per-team keys, budgets and spend attribution.&lt;/p&gt;

&lt;p&gt;Reach for LiteLLM when that shared state is what you need, and then treat the database and its restore as the load-bearing part.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operate it yourself, or have it operated
&lt;/h3&gt;

&lt;p&gt;YNVAR can operate the container, TLS boundary, PostgreSQL, backups and upgrade window on European infrastructure. The customer keeps the provider accounts, provider keys, virtual-key policy and model-access decisions.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.ynvar.com/blog/litellm-gateway-for-teams/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=ynvar_earned_media_2026q3&amp;amp;utm_content=devto-litellm" rel="noopener noreferrer"&gt;full write-up, including the backup scope and the operate-versus-own split&lt;/a&gt; goes further than this post.&lt;/p&gt;

&lt;p&gt;European hosting documents the location of that platform layer. It does not determine where an external model provider processes requests, and it is not a GDPR-compliance claim.&lt;/p&gt;

&lt;p&gt;YNVAR is not an official partner of the LiteLLM project or of any model provider.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;— Adrians Petrovs, Founder and Infrastructure Engineer at YNVAR&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>devops</category>
      <category>selfhosting</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
