fluent/fluentd · error · Fluent::ConfigError

secondary plugin '#{self.class}' must support buffering, but

Error message

secondary plugin '#{self.class}' must support buffering, but doesn't.

What it means

Fluent::Plugin::Output#configure raises this Fluent::ConfigError when a plugin used inside `<secondary>` supports only synchronous processing (implements #process but neither #write nor #try_write). A secondary plugin receives already-buffered chunks from the failed primary and must flush them itself via #write, so a synchronous-only plugin cannot fulfill the role — unlike error 198, this fires even without a <buffer> section in the secondary itself, because secondaries always operate in buffered mode without their own buffer instance.

Source

Thrown at lib/fluent/plugin/output.rb:277

        @emit_records_metrics = metrics_create(namespace: "fluentd", subsystem: "output", name: "emit_records", help_text: "Number of emit records")
        @emit_size_metrics =  metrics_create(namespace: "fluentd", subsystem: "output", name: "emit_size", help_text: "Total size of emit events")
        @write_count_metrics = metrics_create(namespace: "fluentd", subsystem: "output", name: "write_count", help_text: "Number of writing events")
        @write_secondary_count_metrics = metrics_create(namespace: "fluentd", subsystem: "output", name: "write_secondary_count", help_text: "Number of writing events in secondary")
        @rollback_count_metrics = metrics_create(namespace: "fluentd", subsystem: "output", name: "rollback_count", help_text: "Number of rollbacking operations")
        @flush_time_count_metrics = metrics_create(namespace: "fluentd", subsystem: "output", name: "flush_time_count", help_text: "Count of flush time")
        @slow_flush_count_metrics = metrics_create(namespace: "fluentd", subsystem: "output", name: "slow_flush_count", help_text: "Count of slow flush occurred time(s)")
        @drop_oldest_chunk_count_metrics = metrics_create(namespace: "fluentd", subsystem: "output", name: "drop_oldest_chunk_count", help_text: "Number of count that old chunk were discarded with drop_oldest_chunk")

        if has_buffer_section
          unless implement?(:buffered) || implement?(:delayed_commit)
            raise Fluent::ConfigError, "<buffer> section is configured, but plugin '#{self.class}' doesn't support buffering"
          end
          @buffering = true
        else # no buffer sections
          if implement?(:synchronous)
            if !implement?(:buffered) && !implement?(:delayed_commit)
              if @as_secondary
                raise Fluent::ConfigError, "secondary plugin '#{self.class}' must support buffering, but doesn't."
              end
              @buffering = false
            else
              if @as_secondary
                # secondary plugin always works as buffered plugin without buffer instance
                @buffering = true
              else
                # @buffering.nil? shows that enabling buffering or not will be decided in lazy way in #start
                @buffering = nil
              end
            end
          else # buffered or delayed_commit is supported by `unless` of first line in this method
            @buffering = true
          end
        end
        # Enable to update record size metrics or not
        @enable_size_metrics = !!system_config.enable_size_metrics

View on GitHub (pinned to dd45c6e18d)

Solutions

  1. Use a buffering-capable plugin in <secondary>: @type file, @type secondary_file (the purpose-built one), or a network output implementing #write
  2. To fan out failed chunks, make the primary's normal path handle fan-out (copy in the <match>) and keep the secondary simple
  3. Plugin authors: implement #write(chunk) so the plugin qualifies as buffered

Example fix

# before
<secondary>
  @type copy           # copy is process-only; secondary must implement write
  <store> @type stdout </store>
</secondary>

# after
<secondary>
  @type file
  path /var/log/fluent/failed
</secondary>
Defensive patterns

Strategy: type-guard

Validate before calling

# Validate secondary candidates before deploying config
secondary_type = secondary_elem['@type']
plugin = Fluent::Plugin.new_output(secondary_type)
unless plugin.implement?(:buffered) || plugin.implement?(:delayed_commit)
  raise "#{secondary_type} cannot be used in <secondary>: it does not support buffering"
end

Type guard

# Type guard for secondary eligibility (Ruby)
def secondary_capable?(output_type)
  p = Fluent::Plugin.new_output(output_type)
  p.implement?(:buffered) || p.implement?(:delayed_commit)
end

secondary_capable?('file')           # => true
secondary_capable?('secondary_file') # => true
secondary_capable?('copy')           # => false (process-only)

Try / catch

# In config tests:
begin
  driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::ForwardOutput)
  driver.configure(fluent_conf_with_secondary)
rescue Fluent::ConfigError => e
  fail "secondary rejected: #{e.message}"
end

Prevention

When it happens

Trigger: `<secondary> @type copy ... </secondary>` (out_copy defines only #process — the canonical trigger), or any process-only custom plugin placed in <secondary>. Note @type file, secondary_file, stdout, null are buffered-capable and fine.

Common situations: Trying to fan out failed chunks to multiple destinations with copy inside <secondary>; promoting a custom sync plugin to secondary duty during incident response; misreading the secondary docs' plugin list.

Related errors


AI-assisted analysis of fluent/fluentd@dd45c6e18d (2026-08-21). Data as JSON: /api/errors/d9d4e5182a7a334e. Report an issue: GitHub.