DCR at Scale: Auth0, Client Lifecycle, and the Cleanup Problem

May 23, 2026
Walter Manger
5 minute read

      Dynamic Client Registration (DCR) is elegant: an MCP agent connects, registers itself with your OAuth provider, gets credentials, and goes to work. No manual secret management. No static credentials lying around.

      But there’s a hidden cost. Every time an agent starts, it creates a new OAuth client. If the agent runs 100 times a day, you’ve created 100 clients. After a month, you have 3,000 clients in your OAuth provider. After a year, 36,000.

      Most of these are dead. The agent ran once, completed its task, and never ran again. But the client it registered? Still there. Still consuming quota. Still a potential security surface.

      This is the client lifecycle management problem.

      The Problem: OAuth Clients as Garbage

      When you register a client via DCR, you’re creating a long-lived object in your OAuth provider. The client has credentials, permissions, metadata, an audit trail.

      For ephemeral agents, this is wasteful. The agent might run once. It might run a thousand times. But from the OAuth provider’s perspective, all those clients are permanent residents.

      At scale, this creates several problems:

      Quota bloat: OAuth providers often rate-limit the number of clients per tenant. If you’re creating a new client per agent session, you’ll hit that limit. As your user base grows, so do the applications created by DCR. Auth0, for example, has limits on the number of applications per tenant.

      Audit trail pollution: Each client has an audit trail of when it was created, used, and last accessed. With thousands of dead clients, finding signal in the noise becomes harder.

      Security surface: A dead client is still a credential pair in your system. If its secret ever leaked, the credential is still valid. You can’t audit which agent (if any) is actually using it.

      Operational overhead: If you need to rotate credentials or revoke a set of clients, you’re hunting through thousands of entries.

      The Solution: Last-Login Metadata + Background Cleanup

      Here’s how we solved it at VideoAmp:

      1. Every time an MCP agent uses a client, update a `last_login` timestamp in the client’s metadata.
      2. Run a background job daily that deletes any client where `last_login < 30 days`.

      The flow:

      • Agent connects to MCP server
      • MCP server calls DCR, gets a new client
      • Agent uses the client to authenticate (makes an API call)
      • Auth0 Action intercepts the authentication
      • Action updates the client’s `app_metadata.last_login` to the current timestamp
      • Next day’s cleanup job runs, queries all clients, filters to those with `last_login < 30 days ago`, and deletes them

      Implementing with Auth0 Actions

      Auth0 Actions let you execute code in response to authentication events. The flow:

      1. Create an Action that fires on the “Client Credentials Exchange” event:
      exports.onExecuteCredentialsExchange = async (event, api) => {
        // Update the client's last_login metadata
        const { client_id } = event.client;
      
        // Fetch the current client to get existing metadata
        const client = await api.clientsV2.get({ client_id });
      
        // Update app_metadata with last_login
        const updated_metadata = {
          ...client.app_metadata,
          last_login: new Date().toISOString()
        };
      
        // Update the client
        await api.clientsV2.update({ client_id }, {
          app_metadata: updated_metadata
        });
      };
      

      Now every time an MCP agent authenticates, the client’s `last_login` is updated.

      1. Create a background job (cron, Lambda, whatever) that runs daily:
      import requests
      from datetime import datetime, timedelta
      
      # Get Auth0 management token
      auth0_domain = "your-tenant.auth0.com"
      client_id = "your-management-client"
      client_secret = "your-management-secret"
      
      token_response = requests.post(
          f"https://{auth0_domain}/oauth/token",
          json={
              "client_id": client_id,
              "client_secret": client_secret,
              "audience": f"https://{auth0_domain}/api/v2/",
              "grant_type": "client_credentials"
          }
      )
      access_token = token_response.json()["access_token"]
      
      # Get all clients
      clients_response = requests.get(
          f"https://{auth0_domain}/api/v2/clients",
          headers={"Authorization": f"Bearer {access_token}"},
          params={"per_page": 100}
      )
      
      clients = clients_response.json()
      cutoff_date = datetime.utcnow() - timedelta(days=30)
      
      # Delete stale clients
      for client in clients:
          app_metadata = client.get("app_metadata", {})
          last_login_str = app_metadata.get("last_login")
      
          if not last_login_str:
              # No last_login recorded — assume stale
              continue
      
          last_login = datetime.fromisoformat(last_login_str.replace("Z", "+00:00"))
      
          if last_login < cutoff_date:
              # Client is stale, delete it
              requests.delete(
                  f"https://{auth0_domain}/api/v2/clients/{client['client_id']}",
                  headers={"Authorization": f"Bearer {access_token}"}
              )
              print(f"Deleted stale client: {client['client_id']}")
      

      This job runs daily, identifies any client with `last_login` older than 30 days, and removes it.

      Why This Works

      Automated: No manual intervention. Cleanup happens silently.

      Configurable: Change the cutoff from 30 days to 7 days or 60 days depending on your needs.

      Audit trail: Auth0 logs the client deletion, so you have a record of what was cleaned up and when.

      Prevents quota issues: You’re constantly pruning dead clients, preventing the bloom.

      Keeps the surface small: At any given time, you only have clients that have been used in the last 30 days.

      Edge Cases

      What if an agent legitimately doesn’t run for 30 days? Set the cutoff appropriately. If you have agents that run monthly, use a 45-day window instead of 30.

      What if an agent is revoked but should stay in the system for audit reasons? Before deleting, you could archive clients instead (set a `deleted_at` flag, move them to a separate directory). But for most cases, hard delete is fine.

      What if the Action fails to update metadata? The agent can still authenticate — the Action failure doesn’t block the exchange. But the client won’t get updated, and it’ll eventually be cleaned up. That’s acceptable.

      The Bigger Picture

      This is a pattern you’ll see more often as agents proliferate. DCR solves the authentication problem (how do agents get credentials), but it creates an operational problem (how do you manage the lifecycle of thousands of short-lived clients).

      The solution is always the same: metadata + background cleanup. Mark things with timestamps. Run cleanup jobs. Keep the surface small.

      If you’re building with DCR, plan for this from day one. Your future self will thank you.