Bigtable options#

Every option the Bigtable sink and source take. What each one is for is on the Bigtable connector page; the three forms of the Default column are explained here.

The WITH options of the bigtable table connector are a separate surface, documented on the Bigtable SQL connector page.

One thing this reference does not list, because it is not an option here. There are no retry knobs: the client retries MutateRows per entry on a schedule of its own, so the sink owns no retry loop — the opposite of the Cloud Tasks sink, and the reasoning is under Retries. The recovery* knobs below are not an exception: they budget the sink-owned table auto-creation repair, not the client’s mutation retries. The source owns no retry loop either: the client resumes a broken ReadRows stream from the last key it saw.

BigtableSink.builder()#

OptionDefaultWhat it does
tablerequired, unless destinationResolver is setWrites every mutation to one fixed table
destinationResolver—Resolves the table per record. Runs before the serializer; returning null fails the job
serializerrequiredTurns a record into a RowMutationEntry, or into null to skip it
appProfileIdunset ⇒ the instance’s default profileThe application profile the client routes through, which is what selects the routing policy and the request priority. A Data Boost profile is read-only — its eligible methods are ReadRows, SampleRowKeys and PingAndWarm, and it carries neither a request priority nor a routing policy of its own — so naming one here breaks writes. The connector cannot tell locally what kind a profile is, which is why this is documented rather than rejected at build()
serviceAccountKeyFileunset ⇒ application-default credentialsReads a service-account JSON key when each writer starts and shares it with that writer’s data and table-admin clients. Every eligible TaskManager must see the same path. Rejected beside emulatorEndpoint; see the deployment note
writerOptionsdefaultsThe batch thresholds and the in-flight bounds
failedMutationHandlerFailureHandler.failJob()What happens to a mutation that terminally fails. Only the two data-shaped failures reach it — see Error handling. The queue behind sendToDeadLetterQueue(...) has options of its own
emulatorEndpoint—Points the sink at an emulator over a plaintext channel with no credentials. Never production. Given as host:port, and rejected at the setter if it is not
createDispositionCREATE_NEVERWhether a missing table or column family is created (CREATE_IF_NEEDED) or fails the job. CREATE_IF_NEEDED requires tableCreateOptions
tableCreateOptions—The column families and rules for the table the sink creates. Required with CREATE_IF_NEEDED, rejected with CREATE_NEVER

The mutation itself is built by the serializer, not configured here. Row key, column families and qualifiers, cell timestamps, deletes — every per-record decision about the mutation belongs to the BigtableSerializationSchema, which returns the whole RowMutationEntry; the table it goes to is the resolver’s, never the serializer’s. The one decision worth making deliberately is the cell timestamp, because it is what decides whether a replayed record overwrites a cell or adds a version to it; see Delivery guarantees.

A destination costs a batcher. The client binds a bulk mutation batcher to one table, so the writer holds one per table a resolver names, over a client shared by the tables of an instance; an idle table’s batcher is dropped after destinationIdleTimeout, and the client normally starts closing on a daemon reaper when its last live table is gone. If the runtime refuses that handoff, the factory closes the client on the task thread to avoid leaking it. maxActiveInstances bounds open and closing clients held by each writer subtask; a physical close keeps its slot, so client creation waits interruptibly when every slot is still occupied. The in-flight bounds below are the writer’s, summed across destinations rather than split among them. See Per-record destinations.

BigtableWriterOptions#

Set through writerOptions(...); every knob is defaulted, so defaults() is equivalent to not setting options at all.

OptionDefaultWhat it does
batchElementCountThresholdunset ⇒ 100 (the client’s threshold)How many entries — one per record written, whatever each carries — the client accumulates before sending a batch. At most 19999
batchRequestByteThresholdunset ⇒ 20 MiB (the client’s threshold)How many bytes of mutations it accumulates before sending a batch. At most 104857599 — one byte under 100 MiB
maxInFlightEntries1000Caps unacknowledged entries. At the cap write() yields to the task mailbox. Above 20000 the sink logs a WARN — see below
maxInFlightBytes64 MiBCaps their serialized size, which is the bound that actually bounds memory. Above 100 MiB the sink logs a WARN — see below
maxConsecutiveRejections100Fails the job once this many confirmed rejections arrive in a row with no applied mutation between them — the guardrail on a dropping policy’s isolation cost. Any success resets the count; -1 removes the bound
recoveryInitialBackoff500 msFirst backoff of the table auto-creation recovery: re-applying mutations after creating a missing table
recoveryMaxBackoff10 sIts backoff cap; must be at least the initial backoff
recoveryMaxAttempts10Its attempt cap, after which the job fails with the incident’s cause. One repair covers every table an incident left missing and shares this budget across them; a post-ensure missing-family response for an undeclared, absent family fails immediately instead
destinationIdleTimeout1 hHow long a table may go without mutations before the writer drops its batcher. Swept at the end of a checkpoint’s flush; an evicted table rebuilds transparently. To never evict, set a very large duration — up to Duration.ofNanos(Long.MAX_VALUE)
maxActiveInstances16Caps open-or-closing instance clients per writer subtask. At capacity the writer drains outstanding mutations and evicts the least recently used instance; client creation waits interruptibly until physical close frees the slot. Many tables sharing one instance consume one slot
perDestinationMetricsfalseRegisters per-table recordsSend and sendErrors counters beside the writer’s totals. Off by default: Flink cannot unregister a metric, so with a resolver every table the job writes to keeps a row in the registry for the task’s lifetime. See Metrics

Every count in this table counts entries, not mutations. An entry is one RowMutationEntry — one record the serializer returned — and it carries as many mutations as the serializer put setCell calls in it. Bigtable’s own documented limit is on mutations: no more than 100,000 in a batch. The two numbers never have to be reconciled by a job, because the client holds a batch to that limit itself, whatever batchElementCountThreshold says; the measurement and what it retires are under Tuning.

Raising maxInFlightEntries far above its default does not raise the effective bound; it moves it. The client has a flow controller of its own — 20,000 outstanding entries and 100 MiB, and it blocks the calling thread when either is reached — whose limits its public API does not expose. While the sink’s own bounds are the tighter pair, a full writer yields to the task mailbox, which is what keeps checkpoint barriers moving; past them, the task thread stalls inside the client instead. The reasoning is under Tuning.

Those same two budgets are where the batch thresholds’ ceilings come from, and they are the client’s rule rather than this connector’s: its settings builder requires each threshold to stay strictly below the matching budget, and refuses to build a client at all otherwise — on the task manager, as the writer opens. Hence 19,999 and 100 MiB − 1: one under each.

The in-flight bounds are warned about rather than capped at those same figures. Setting either above its budget is a working configuration — the client simply becomes the layer that bounds the sink — so build() logs a WARN naming the value and what it costs instead of rejecting it. It is not a ceiling because the budget is per client and this sink holds one per (project, instance): a resolver spreading records over several instances draws on several budgets, and a writer-global bound above one of them can be what that job means.

The two batch thresholds are left unset by default rather than restated here, so a client upgrade that retunes them is inherited. Lowering batchElementCountThreshold shortens the delay before a mutation reaches the service at low volume; the client also sends a batch after one second regardless.

TableCreateOptions#

The schema for the table the sink creates under CREATE_IF_NEEDED — its column families and, per family, an optional garbage-collection rule. Creation only, per family: an existing table is written to as it is, except that families declared here which it lacks are added, with their rules; an existing family’s rule is neither compared nor updated. What creation does and does not repair is under Table auto-creation.

OptionDefaultWhat it does
columnFamilyat least one requiredDeclares a column family, optionally with a GcRule. Repeatable; a repeated name is last-writer-wins

A family declared without a rule keeps Bigtable’s default of collecting nothing — for this at-least-once sink a real decision, since the garbage-collection policy is what decides whether a replay’s duplicate cell versions accumulate forever. The rule is built from four static GcRule factories mirroring the admin API’s shapes: maxVersions(int), maxAge(Duration), union(GcRule...) and intersection(GcRule...) (each composite takes at least two rules). union(GcRule.maxVersions(1), GcRule.maxAge(...)) is the usual shape for keeping only the latest cell. Validation is shape-only — positivity and arity; the service’s own limits are left to Bigtable, whose rejection names what it refused.

BigtableSource.builder()#

OptionDefaultWhat it does
tablerequiredThe table to read
deserializerrequiredTurns a row into zero or more non-null records. Emit synchronously during the call; do not retain the collector. Emitting nothing skips the row
rowRangeunset ⇒ the whole tableAdds a row-key range to read. Repeatable and additive; overlapping ranges are merged, and an empty one is rejected at build(). Takes a ByteStringRange, or an inclusive start and an exclusive end as ByteStrings or as UTF-8 text
prefixunset ⇒ the whole tableAdds every row whose key starts with a prefix — sugar for the range that prefix describes. Repeatable, and combinable with rowRange
filter—One server-side Filters.Filter, applied to every split. What it excludes never leaves the server. Last writer wins; a filter too large for the service is rejected at build()
appProfileIdunset ⇒ the instance’s default profileThe application profile the client routes through. A Data Boost profile is named here like any other
maxRowsPerFetch1000Maximum input rows one fetch hands to Flink’s element queue. The fetch returns when this count or maxBytesPerFetch is reached
maxBytesPerFetch8 MiBTarget maximum decoded input bytes one fetch hands to Flink’s element queue. One row larger than the target is handed over alone
serviceAccountKeyFileunset ⇒ application-default credentialsReads a service-account JSON key when the JobManager’s enumerator or a TaskManager’s reader starts. Every eligible process must see the same path. Rejected beside emulatorEndpoint; see the deployment note
emulatorEndpoint—Points the source at an emulator over a plaintext channel with no credentials. Never production. Given as host:port, and rejected at the setter if it is not

The fetch bounds are memory hand-off controls, not a query row limit. A Query.limit() is global to a query, so it cannot be partitioned across splits without coordination — the client library refuses to shard a query that carries one. The source instead reads the complete configured ranges and returns control to Flink whenever either fetch bound is reached.

Per-cell shaping is the filter’s job, not a knob’s. Which families and qualifiers to return, which timestamp window, how many versions of a cell — all of it is expressible through filter(...), which is why the source has no separate options for any of it.

The SQL layer’s scan.* options map onto this builder — the Bigtable SQL connector page carries that surface, and its projection pushdown is what supplies filter(...) there.

BigtableChangeStreamSource.builder()#

@PublicEvolving: the change-stream API may change at a minor release, announced in the release notes.

OptionDefaultWhat it does
tablerequiredThe change-stream-enabled table to read
deserializerrequiredTurns each BigtableChangeStreamMutation into zero or more non-null records. Emit synchronously during the call; do not retain the collector
appProfileIdrequiredA single-cluster-routing application profile used by every change-stream RPC
serviceAccountKeyFileunset ⇒ application-default credentialsReads a service-account JSON key when the JobManager’s coordinator or a TaskManager’s reader starts. Each component shares the provider among the data, table-admin and instance-admin clients that it owns, and every eligible process must see the same path. See the deployment note
startPositionStartPosition.latest()The position used only for a fresh job: latest, earliest, an absolute instant, or a duration ago
resumeFallback—Explicitly restarts an expired checkpointed partition at this position, discarding any stale token it held. Without it, an expired restore fails
boundedTimestamp—Stops at this instant and makes the source bounded. Without it, the source is continuous
maxConcurrentStreamsPerSubtask2Bounds open ReadChangeStream RPCs in each source subtask. Source parallelism multiplied by this value is configured job-wide read capacity, not a Bigtable quota
familyIncludeListemptyRetains mutation entries whose family name fully matches at least one Java regular expression. Mutually exclusive with familyExcludeList
familyExcludeListemptyRemoves mutation entries whose family name fully matches any Java regular expression. Mutually exclusive with familyIncludeList
qualifierIncludeListemptyRetains qualified mutation entries whose family:qualifierBase64 identifier fully matches at least one Java regular expression. The qualifier uses canonical padded RFC 4648 standard Base64. Mutually exclusive with qualifierExcludeList
qualifierExcludeListemptyRemoves qualified mutation entries whose family:qualifierBase64 identifier fully matches any Java regular expression. Family-delete entries have no qualifier and use only the family filter. Mutually exclusive with qualifierIncludeList
skipMessagesWithoutChangefalseSkips deserialization when filtering removes every entry. The default delivers the mutation with an empty entry list

There is no emulator option because the Bigtable emulator implements neither change-stream RPC.

There is no heartbeat-interval option either, because the five-second service heartbeat is also what paces the reader’s rotation of queued partitions.