{"record":{"id":"b1f7a4c470b8fa6c","repo":"etcd-io/etcd","slug":"unexpected-limit-in-delete","errorCode":null,"errorMessage":"unexpected limit in delete","messagePattern":"unexpected limit in delete","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":279,"sourceCode":"\t}\n\tret := Op{t: tRange, key: []byte(key)}\n\tret.applyOpts(opts)\n\treturn ret\n}\n\n// OpDelete returns \"delete\" operation based on given key and operation options.\nfunc OpDelete(key string, opts ...OpOption) Op {\n\t// WithPrefix and WithFromKey are not supported together\n\tif IsOptsWithPrefix(opts) && IsOptsWithFromKey(opts) {\n\t\tpanic(\"`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one\")\n\t}\n\tret := Op{t: tDeleteRange, key: []byte(key)}\n\tret.applyOpts(opts)\n\tswitch {\n\tcase ret.leaseID != 0:\n\t\tpanic(\"unexpected lease in delete\")\n\tcase ret.limit != 0:\n\t\tpanic(\"unexpected limit in delete\")\n\tcase ret.rev != 0:\n\t\tpanic(\"unexpected revision in delete\")\n\tcase ret.sort != nil:\n\t\tpanic(\"unexpected sort in delete\")\n\tcase ret.serializable:\n\t\tpanic(\"unexpected serializable in delete\")\n\tcase ret.countOnly:\n\t\tpanic(\"unexpected countOnly in delete\")\n\tcase ret.minModRev != 0, ret.maxModRev != 0:\n\t\tpanic(\"unexpected mod revision filter in delete\")\n\tcase ret.minCreateRev != 0, ret.maxCreateRev != 0:\n\t\tpanic(\"unexpected create revision filter in delete\")\n\tcase ret.filterDelete, ret.filterPut:\n\t\tpanic(\"unexpected filter in delete\")\n\tcase ret.createdNotify:\n\t\tpanic(\"unexpected createdNotify in delete\")\n\t}\n\treturn ret","sourceCodeStart":261,"sourceCodeEnd":297,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L261-L297","documentation":"OpDelete rejects WithLimit because limit caps the number of results returned by a range query; a delete range has no result set to cap. After applyOpts stores the limit, the post-condition switch sees ret.limit != 0 and panics with \"unexpected limit in delete\". This guards against options that would be silently ignored (or worse, mis-encoded) in the delete request.","triggerScenarios":"clientv3.OpDelete(\"/jobs/\", clientv3.WithPrefix(), clientv3.WithLimit(100)) — typically from an attempt to 'delete in batches'; or cli.Delete(ctx, k, sharedOpts...) where sharedOpts was built for pagination of a Get.","commonSituations":"Developers wanting bounded/batched deletes to limit server load copy the Get pagination pattern onto Delete; generic option forwarding between list and purge code paths.","solutions":["Drop WithLimit from Delete calls — etcd delete ranges are atomic and unbounded by design.","For batched deletion, first page through keys with Get+WithLimit, then issue Txn deletes of those explicit keys (accepting it is no longer one atomic range delete).","Keep pagination options scoped to Get call sites only."],"exampleFix":"// before\n_, err := cli.Delete(ctx, \"/jobs/\", clientv3.WithPrefix(), clientv3.WithLimit(500)) // panics\n\n// after — atomic full-range delete\n_, err := cli.Delete(ctx, \"/jobs/\", clientv3.WithPrefix())\n// or explicit batching via Get+Txn if you truly need bounded chunks","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["Delete ranges cannot be capped; do not port Get pagination options onto Delete.","For bounded deletion, Get a page of keys then delete those explicit keys in a Txn.","Scope WithLimit strictly to Get call sites."],"tags":["go","etcd","clientv3","delete","limit","pagination","panic","invalid-option"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}