Introduction

ActiveJob uses GlobalID to serialize ActiveRecord model references in job arguments. Instead of serializing the entire object, GlobalID stores a URI-like string (gid://app/User/123) that is resolved back to the model when the job executes. When GlobalID is misconfigured, the model does not include GlobalID::Identification, or the record is deleted before the job runs, deserialization fails with GlobalID::Errors::MissingURIError or ActiveJob::DeserializationError.

Symptoms

  • GlobalID::Errors::MissingURIError: Unable to serialize without a valid GlobalID
  • ActiveJob::DeserializationError: Error while trying to deserialize arguments: Couldn't find User with 'id'=123
  • Job arguments contain raw hashes instead of GlobalID references
  • Custom model classes missing include GlobalID::Identification
  • Jobs enqueued with deleted records fail on execution

Error in Sidekiq logs: `` 2026-04-09T10:15:00.000Z pid=1234 class=SendWelcomeEmailJob jid=abc123 ERROR: ActiveJob::DeserializationError: Error while trying to deserialize arguments: Couldn't find User with 'id'=456 GlobalID::Errors::MissingURIError: Unable to find a valid URI for #<User:0x00007f>

Common Causes

  • Model does not have an id column or primary key set
  • GlobalID::Identification not included in non-ActiveRecord models
  • Record deleted between job enqueue and execution
  • Custom to_global_id method returning invalid URI
  • Multi-database setup where GlobalID app name does not match

Step-by-Step Fix

  1. 1.Ensure model includes GlobalID::Identification:
  2. 2.```ruby
  3. 3.# For non-ActiveRecord models
  4. 4.class ExternalService
  5. 5.include GlobalID::Identification

attr_accessor :id, :name

def initialize(id:, name:) @id = id @name = name end

# Required by GlobalID def self.find(id) # Custom lookup logic ExternalService.new(id: id, name: "Service #{id}") end end ```

  1. 1.Handle deleted records gracefully in jobs:
  2. 2.```ruby
  3. 3.class SendWelcomeEmailJob < ApplicationJob
  4. 4.retry_on ActiveJob::DeserializationError, wait: :exponentially_long, attempts: 3

discard_on ActiveJob::DeserializationError do |job, error| Rails.logger.warn( "Discarding #{job.class} - record no longer exists: #{error.message}" ) end

def perform(user) # user is deserialized from GlobalID # If record was deleted, ActiveJob::DeserializationError is raised UserMailer.welcome(user).deliver_later rescue ActiveJob::DeserializationError Rails.logger.info("User was deleted before welcome email could be sent") end end ```

  1. 1.Configure GlobalID app identifier for multi-database setups:
  2. 2.```ruby
  3. 3.# config/initializers/global_id.rb
  4. 4.GlobalID.app = Rails.application.class.module_parent_name.downcase

# For multi-database, specify the database in GlobalID class User < ApplicationRecord self.global_id_model_name = "primary_db/user"

def to_global_id(options = {}) super(options.merge(app: "primary_app")) end end ```

  1. 1.Pass ID directly instead of relying on GlobalID:
  2. 2.```ruby
  3. 3.# When GlobalID is unreliable, pass ID explicitly
  4. 4.class NotifyUserJob < ApplicationJob
  5. 5.# Accept ID instead of object
  6. 6.def perform(user_id)
  7. 7.user = User.find_by(id: user_id)
  8. 8.return if user.nil? # Gracefully handle deleted records

NotificationService.call(user) end end

# Enqueue with ID NotifyUserJob.perform_later(user.id)

# Or use rescue_from in ApplicationJob class ApplicationJob < ActiveJob::Base rescue_from ActiveRecord::RecordNotFound do Rails.logger.warn("Record not found for job #{self.class}") end end ```

  1. 1.Debug GlobalID serialization issues:
  2. 2.```ruby
  3. 3.# In Rails console
  4. 4.user = User.first

# Check if GlobalID works user.to_global_id # => #<GlobalID:0x00007f @uri=#<URI::GID gid://myapp/User/1>>

# Verify model responds to required methods user.respond_to?(:id) # => true user.respond_to?(:model_name) # => true user.class.respond_to?(:find) # => true

# Test round-trip gid = user.to_global_id GlobalID::Locator.locate(gid) # => #<User id: 1, ...> ```

Prevention

  • Always check if record exists at job execution time, not just at enqueue
  • Use discard_on ActiveJob::DeserializationError for non-critical jobs
  • Prefer passing IDs over model objects for jobs that outlive the request
  • Add GlobalID validation to model specs: expect(user.to_global_id).to be_valid
  • Monitor DeserializationError rates in error tracking service
  • Document which jobs require GlobalID and which use plain IDs