sidekiq/sidekiq · error · ArgumentError

Bulk arguments must be an Array of Arrays: [[1], [2]]

Error message

Bulk arguments must be an Array of Arrays: [[1], [2]]

What it means

push_bulk's "args" must be an Array of Arrays — one inner Array per job containing that job's arguments (e.g. [[1], [2]] for two single-argument jobs). Inside each batch slice Sidekiq validates every element is an Array and raises ArgumentError otherwise, because each element is merged verbatim as one job's "args".

Source

Thrown at lib/sidekiq/client.rb:165

      spread_interval = items.delete(:spread_interval) || items.delete("spread_interval")
      raise ArgumentError, "Jobs 'spread_interval' must be a positive Numeric" if spread_interval && (!spread_interval.is_a?(Numeric) || spread_interval <= 0)
      raise ArgumentError, "Only one of 'at' or 'spread_interval' can be provided" if at && spread_interval

      if !at && spread_interval
        # Do not use spread interval smaller than pooling interval.
        spread_interval = [spread_interval, 5].max
        now = Time.now.to_f
        at = args.map { now + rand * spread_interval }
      end

      # Use a smaller batch size by default for scheduled jobs since adding to sorted sets is more costly.
      batch_size = items.delete(:batch_size) || items.delete("batch_size") || (at ? 100 : 1_000)

      normed = normalize_item(items)
      slice_index = 0
      result = args.each_slice(batch_size).flat_map do |slice|
        raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless slice.is_a?(Array) && slice.all?(Array)
        break [] if slice.empty? # no jobs to push

        payloads = slice.map.with_index { |job_args, index|
          copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12))
          copy["at"] = (at.is_a?(Array) ? at[slice_index + index] : at) if at
          result = middleware.invoke(items["class"], copy, copy["queue"], @redis_pool) do
            verify_json(copy)
            copy
          end
          result || nil
        }
        slice_index += batch_size

        to_push = payloads.compact
        raw_push(to_push) unless to_push.empty?
        payloads.map { |payload| payload&.[]("jid") }
      end

View on GitHub (pinned to 1bb4aa06e5)

Solutions

  1. Wrap single arguments: "args" => ids.map { |id| [id] }
  2. For multi-arg jobs, one inner Array per job: "args" => [[1, "a"], [2, "b"]]
  3. If jobs take heterogeneous shapes, build the array explicitly rather than transforming flat input automatically
  4. Add a guard at the call site: raise unless args.all? { |a| a.is_a?(Array) }

Example fix

# before
Sidekiq::Client.push_bulk("class" => MyJob, "args" => [1, 2, 3]) # flat -> ArgumentError

# after
Sidekiq::Client.push_bulk("class" => MyJob, "args" => [[1], [2], [3]])
Defensive patterns

Strategy: type-guard

Validate before calling

raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless bulk_args?(args)
Sidekiq::Client.push_bulk("class" => MyJob, "args" => args)

Type guard

def bulk_args?(args)
  args.is_a?(Array) && args.all? { |a| a.is_a?(Array) }
end

Prevention

When it happens

Trigger: push_bulk("class" => MyJob, "args" => [1, 2, 3]) — flat array; or mixed content like "args" => [1, [2, 3]]. Note: the doc comment example "args" => (1..100_000).to_a does not match this stricter code path — the enforced contract is Arrays of Arrays.

Common situations: The single most common push_bulk mistake: passing a flat list where each element is one job's single argument; adapting code from MyJob.perform_bulk or ActiveJob's perform_all_later that accept flat lists; API payloads where each job takes exactly one scalar.

Related errors


AI-assisted analysis of sidekiq/sidekiq@1bb4aa06e5 (2026-08-21). Data as JSON: /api/errors/c2fea5a61ebb2bd4. Report an issue: GitHub.