Sync Data Best Practices
Changing fields in specifics
The Sync Model API uses protobufs to communicate with Chrome services, which is nice because protobufs were written to be robust against protocol changes (see protobuf docs for details). However, once you start syncing data, changing your protobuf format isn't completely painless; not only can sync users upgrade from one Chrome version to another, but they may have different Chrome versions running at the same time! Fortunately, there are some best practices to help make it easy:
- Follow the Sync Protocol Style guide
for
.protoschema rules (adding, deprecating, or migrating fields and enums). - Follow Protection against data override by old Sync clients so older clients preserve unrecognized or partially-supported fields when committing local updates.
- Migrating fields (backwards compatibility): When replacing an existing field with a new one, newer clients must continue populating both the deprecated field and the new field on commit while older clients remain within the Sync support horizon. When reading, prefer the new field if present and fall back to the deprecated field (or reconcile both if an older client updated only the deprecated field).
- Defensive deserialization (forwards & backwards compatibility):
Incoming remote data in
IsEntityDataValid,MergeFullSyncData, andApplyIncrementalSyncChangesmay originate from older or newer clients and must neverCHECK-fail or crash:- Older clients may omit optional fields added in newer versions; always provide sensible defaults when reading.
- Newer clients may send unrecognized enum values (which
proto2places inunknown_fields(), returning the defaultFOO_UNSPECIFIED = 0value from the getter) or unrecognizedoneofentity subtypes (which parse asNOT_SET). IsEntityDataValidshould validate required identity invariants (e.g., a non-empty GUID/key) but must not reject entities solely because a oneofsubtype isNOT_SETif new subtypes may be added in the future. IfIsEntityDataValidreturnsfalse, the processor drops the update entirely without caching its sync metadata. Instead, accept the entity inIsEntityDataValidso its metadata is tracked, and ignore the unrecognized subtype when updating the local model.
- Client tag immutability: Never change how
GetClientTagis derived once a data type has launched, and only derive it from immutable fields that all client versions populate identically. ChangingGetClientTagchanges theclient_tag_hashand causes duplicate entities across clients.
Conflict resolution
A conflict can occur when an entity has a pending local commit and an update for
the same entity comes from another client during an incremental update. There is
a default implementation of conflict resolution which is sufficient in most
cases. However it's possible to provide in the bridge a custom
ResolveConflict implementation. The result can be either
keeping the local pending change, or fully apply the remote update. Note that
currently merging is not supported during the conflict resolution.
It is recommended to avoid any additional logic to detect conflicts or merge local data with remote updates. This may lead to inconsistency between the client and the server, and hence between different clients. Re-uploading of such merged data should also be avoided because it may result in a ping-pong issue when two different clients would indefinitely reupload merged results of each other.
Avoiding ping-pong
tl;dr: Do not call change_processor()->Put(..) in your
ApplyIncrementalSyncChanges implementation! In other words, an incoming sync
update must never directly lead to an outgoing update.
Sometimes, incoming changes from the server can be "bad" in some way - for
example, some older version of Chrome committed invalid or incomplete data. In
such cases, it's tempting to fix the data, by committing a "fixed" version
directly when receiving the "bad" data in ApplyIncrementalSyncChanges.
However, doing this can lead to "ping-pong" issues: If two clients try to fix
bad data, but disagree on what the correct state is, they'll continuously
re-update the same entity back and forth, as fast as their network connections
allow. If enough clients get into this state, this amounts to a DDoS attack on
the sync server.
Note that it's effectively impossible to ensure that no past or future Chrome
version disagrees with your current code's behavior.
Instead, it's usually best to fix up the data locally without committing it, and relying on the next natural change to commit the fixed data to the server. If you absolutely must re-upload the fixed data, this must be rate-limited in some way, e.g. do it at most once per browser startup.
Entity size limits
The Chrome Sync server enforces a hard 10 KB (10,240-byte) limit per
serialized entity (including SyncEntity metadata and server wrapper
attributes). If a committed entity exceeds 10 KB, the server silently drops the
write and returns a fake SUCCESS response to prevent infinite client retry
loops, resulting in silent data loss. Note that this limit is subject to
change; if your feature needs to exceed this limit, reach out to the Sync team.
Additionally, when Nigori encryption is active (for users with a custom
passphrase or for always-encrypted data types),
Nigori::Encrypt()
AES-CBC encrypts and Base64-encodes the serialized EntitySpecifics into
EncryptedData.blob, which inflates the payload size by ~33% (4/EntitySpecifics ceiling to ~7–8 KB.
To avoid hitting this limit:
- Keep serialized plaintext
EntitySpecificswell below 8 KB (ideally under a few KB, and strictly below ~7.3 KB if the type can be encrypted). - Enforce upper bounds on variable-length string, bytes, or repeated fields (such as URLs, notes, or descriptions) in the bridge before committing.
- Monitor client-side entity sizes via the
Sync.EntitySizeOnCommit.Entity.SpecificsOnly.<DataType>andSync.EntitySizeOnCommit.Entity.WithMetadata.<DataType>histograms.