{"record":{"id":"c2fea5a61ebb2bd4","repo":"sidekiq/sidekiq","slug":"bulk-arguments-must-be-an-array-of-arrays-1","errorCode":null,"errorMessage":"Bulk arguments must be an Array of Arrays: [[1], [2]]","messagePattern":"Bulk arguments must be an Array of Arrays: \\[\\[1\\], \\[2\\]\\]","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/sidekiq/client.rb","lineNumber":165,"sourceCode":"\n      spread_interval = items.delete(:spread_interval) || items.delete(\"spread_interval\")\n      raise ArgumentError, \"Jobs 'spread_interval' must be a positive Numeric\" if spread_interval && (!spread_interval.is_a?(Numeric) || spread_interval <= 0)\n      raise ArgumentError, \"Only one of 'at' or 'spread_interval' can be provided\" if at && spread_interval\n\n      if !at && spread_interval\n        # Do not use spread interval smaller than pooling interval.\n        spread_interval = [spread_interval, 5].max\n        now = Time.now.to_f\n        at = args.map { now + rand * spread_interval }\n      end\n\n      # Use a smaller batch size by default for scheduled jobs since adding to sorted sets is more costly.\n      batch_size = items.delete(:batch_size) || items.delete(\"batch_size\") || (at ? 100 : 1_000)\n\n      normed = normalize_item(items)\n      slice_index = 0\n      result = args.each_slice(batch_size).flat_map do |slice|\n        raise ArgumentError, \"Bulk arguments must be an Array of Arrays: [[1], [2]]\" unless slice.is_a?(Array) && slice.all?(Array)\n        break [] if slice.empty? # no jobs to push\n\n        payloads = slice.map.with_index { |job_args, index|\n          copy = normed.merge(\"args\" => job_args, \"jid\" => SecureRandom.hex(12))\n          copy[\"at\"] = (at.is_a?(Array) ? at[slice_index + index] : at) if at\n          result = middleware.invoke(items[\"class\"], copy, copy[\"queue\"], @redis_pool) do\n            verify_json(copy)\n            copy\n          end\n          result || nil\n        }\n        slice_index += batch_size\n\n        to_push = payloads.compact\n        raw_push(to_push) unless to_push.empty?\n        payloads.map { |payload| payload&.[](\"jid\") }\n      end\n","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/sidekiq/sidekiq/blob/1bb4aa06e5aa178a114a5e855f9f3d5c24f6c61b/lib/sidekiq/client.rb#L147-L183","documentation":"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\".","triggerScenarios":"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.","commonSituations":"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.","solutions":["Wrap single arguments: \"args\" => ids.map { |id| [id] }","For multi-arg jobs, one inner Array per job: \"args\" => [[1, \"a\"], [2, \"b\"]]","If jobs take heterogeneous shapes, build the array explicitly rather than transforming flat input automatically","Add a guard at the call site: raise unless args.all? { |a| a.is_a?(Array) }"],"exampleFix":"# before\nSidekiq::Client.push_bulk(\"class\" => MyJob, \"args\" => [1, 2, 3]) # flat -> ArgumentError\n\n# after\nSidekiq::Client.push_bulk(\"class\" => MyJob, \"args\" => [[1], [2], [3]])","handlingStrategy":"type-guard","validationCode":"raise ArgumentError, \"Bulk arguments must be an Array of Arrays: [[1], [2]]\" unless bulk_args?(args)\nSidekiq::Client.push_bulk(\"class\" => MyJob, \"args\" => args)","typeGuard":"def bulk_args?(args)\n  args.is_a?(Array) && args.all? { |a| a.is_a?(Array) }\nend","tryCatchPattern":null,"preventionTips":["Wrap single-argument inputs explicitly: ids.map { |id| [id] }","Remember the shape: outer array = jobs, inner array = one job's arguments","Add the bulk_args? guard in shared enqueue helpers so the failure names the call site"],"tags":["validation","push-bulk","argument-format","type-error","sidekiq"],"backgroundTag":"bulk-args-not-nested","analyzedSha":"1bb4aa06e5aa178a114a5e855f9f3d5c24f6c61b","analyzedAt":"2026-08-21T15:49:18.863Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}