diff --git a/example/ipsec/odp_ipsec.c b/example/ipsec/odp_ipsec.c
index 76d27c5..c25f631 100644
--- a/example/ipsec/odp_ipsec.c
+++ b/example/ipsec/odp_ipsec.c
@@ -209,6 +209,33 @@ void free_pkt_ctx(pkt_ctx_t *ctx)
 }
 
 /**
+ * Convert completion event to completed operation packet
+ *
+ * @note Function frees completion event.
+ *
+ * @param completion  Event containing operation results
+ *
+ * @return Packet structure where data now resides or ODP_PACKET_INVALID
+ *         in case of any operation error.
+ */
+static
+odp_packet_t crypto_completion_to_packet(odp_buffer_t completion)
+{
+	odp_crypto_compl_status_t cipher_rc;
+	odp_crypto_compl_status_t auth_rc;
+	odp_packet_t pkt;
+
+	odp_crypto_get_operation_compl_status(completion, &cipher_rc, &auth_rc);
+	if (!is_crypto_compl_status_ok(&cipher_rc))
+		return ODP_PACKET_INVALID;
+	if (!is_crypto_compl_status_ok(&auth_rc))
+		return ODP_PACKET_INVALID;
+	pkt = odp_crypto_get_operation_compl_packet(completion);
+	odp_crypto_operation_compl_free(completion);
+	return pkt;
+}
+
+/**
  * Use socket I/O workaround to query interface MAC address
  *
  * @todo Remove all references to USE_MAC_ADDR_HACK once
@@ -677,18 +704,18 @@ pkt_disposition_e do_route_fwd_db(odp_packet_t pkt, pkt_ctx_t *ctx)
  * @return PKT_CONTINUE if done else PKT_POSTED
  */
 static
-pkt_disposition_e do_ipsec_in_classify(odp_packet_t pkt,
+pkt_disposition_e do_ipsec_in_classify(odp_packet_t *pkt,
 				       pkt_ctx_t *ctx,
 				       bool *skip)
 {
-	uint8_t *buf = odp_packet_addr(pkt);
-	odph_ipv4hdr_t *ip = (odph_ipv4hdr_t *)odp_packet_l3(pkt);
+	uint8_t *buf = odp_packet_addr(*pkt);
+	odp_buffer_t completion = ODP_BUFFER_INVALID;
+	odph_ipv4hdr_t *ip = (odph_ipv4hdr_t *)odp_packet_l3(*pkt);
 	int hdr_len;
 	odph_ahhdr_t *ah = NULL;
 	odph_esphdr_t *esp = NULL;
 	ipsec_cache_entry_t *entry;
 	odp_crypto_op_params_t params;
-	bool posted = 0;
 
 	/* Default to skip IPsec */
 	*skip = TRUE;
@@ -710,8 +737,8 @@ pkt_disposition_e do_ipsec_in_classify(odp_packet_t pkt,
 	/* Initialize parameters block */
 	memset(&params, 0, sizeof(params));
 	params.session = entry->state.session;
-	params.pkt = pkt;
-	params.out_pkt = entry->in_place ? pkt : ODP_PACKET_INVALID;
+	params.pkt = *pkt;
+	params.out_pkt = entry->in_place ? *pkt : ODP_PACKET_INVALID;
 
 	/*Save everything to context */
 	ctx->ipsec.ip_tos = ip->tos;
@@ -743,12 +770,16 @@ pkt_disposition_e do_ipsec_in_classify(odp_packet_t pkt,
 
 	/* Issue crypto request */
 	*skip = FALSE;
-	if (odp_crypto_operation(&params,
-				 &posted,
-				 odp_packet_to_buffer(pkt))) {
+	if (odp_crypto_operation(&params, &completion))
 		abort();
-	}
-	return (posted) ? PKT_POSTED : PKT_CONTINUE;
+	if (completion == ODP_BUFFER_INVALID)
+		return PKT_POSTED;
+
+	*pkt = crypto_completion_to_packet(completion);
+	if (*pkt != ODP_PACKET_INVALID)
+		return PKT_CONTINUE;
+
+	return PKT_DROP;
 }
 
 /**
@@ -763,20 +794,10 @@ static
 pkt_disposition_e do_ipsec_in_finish(odp_packet_t pkt,
 				     pkt_ctx_t *ctx)
 {
-	odp_buffer_t event;
-	odp_crypto_compl_status_t cipher_rc;
-	odp_crypto_compl_status_t auth_rc;
 	odph_ipv4hdr_t *ip;
 	int hdr_len = ctx->ipsec.hdr_len;
 	int trl_len = 0;
 
-	/* Check crypto result */
-	event = odp_packet_to_buffer(pkt);
-	odp_crypto_get_operation_compl_status(event, &cipher_rc, &auth_rc);
-	if (!is_crypto_compl_status_ok(&cipher_rc))
-		return PKT_DROP;
-	if (!is_crypto_compl_status_ok(&auth_rc))
-		return PKT_DROP;
 	ip = (odph_ipv4hdr_t *)odp_packet_l3(pkt);
 
 	/*
@@ -956,11 +977,11 @@ pkt_disposition_e do_ipsec_out_classify(odp_packet_t pkt,
  * @return PKT_CONTINUE if done else PKT_POSTED
  */
 static
-pkt_disposition_e do_ipsec_out_seq(odp_packet_t pkt,
+pkt_disposition_e do_ipsec_out_seq(odp_packet_t *pkt,
 				   pkt_ctx_t *ctx)
 {
-	uint8_t *buf = odp_packet_addr(pkt);
-	bool posted = 0;
+	odp_buffer_t completion = ODP_BUFFER_INVALID;
+	uint8_t *buf = odp_packet_addr(*pkt);
 
 	/* We were dispatched from atomic queue, assign sequence numbers */
 	if (ctx->ipsec.ah_offset) {
@@ -977,12 +998,16 @@ pkt_disposition_e do_ipsec_out_seq(odp_packet_t pkt,
 	}
 
 	/* Issue crypto request */
-	if (odp_crypto_operation(&ctx->ipsec.params,
-				 &posted,
-				 odp_packet_to_buffer(pkt))) {
+	if (odp_crypto_operation(&ctx->ipsec.params, &completion))
 		abort();
-	}
-	return (posted) ? PKT_POSTED : PKT_CONTINUE;
+	if (completion == ODP_BUFFER_INVALID)
+		return PKT_POSTED;
+
+	*pkt = crypto_completion_to_packet(completion);
+	if (*pkt != ODP_PACKET_INVALID)
+		return PKT_CONTINUE;
+
+	return PKT_DROP;
 }
 
 /**
@@ -997,18 +1022,8 @@ static
 pkt_disposition_e do_ipsec_out_finish(odp_packet_t pkt,
 				      pkt_ctx_t *ctx)
 {
-	odp_buffer_t event;
-	odp_crypto_compl_status_t cipher_rc;
-	odp_crypto_compl_status_t auth_rc;
 	odph_ipv4hdr_t *ip;
 
-	/* Check crypto result */
-	event = odp_packet_to_buffer(pkt);
-	odp_crypto_get_operation_compl_status(event, &cipher_rc, &auth_rc);
-	if (!is_crypto_compl_status_ok(&cipher_rc))
-		return PKT_DROP;
-	if (!is_crypto_compl_status_ok(&auth_rc))
-		return PKT_DROP;
 	ip = (odph_ipv4hdr_t *)odp_packet_l3(pkt);
 
 	/* Finalize the IPv4 header */
@@ -1059,7 +1074,13 @@ void *pktio_thread(void *arg EXAMPLE_UNUSED)
 
 		/* Use schedule to get buf from any input queue */
 		buf = SCHEDULE(&dispatchq, ODP_SCHED_WAIT);
-		pkt = odp_packet_from_buffer(buf);
+		if (completionq == dispatchq)
+			pkt = crypto_completion_to_packet(buf);
+		else
+			pkt = odp_packet_from_buffer(buf);
+
+		if (pkt == ODP_PACKET_INVALID)
+			continue;
 
 		/* Determine new work versus completion or sequence number */
 		if ((completionq != dispatchq) && (seqnumq != dispatchq)) {
@@ -1093,7 +1114,7 @@ void *pktio_thread(void *arg EXAMPLE_UNUSED)
 
 			case PKT_STATE_IPSEC_IN_CLASSIFY:
 
-				rc = do_ipsec_in_classify(pkt, ctx, &skip);
+				rc = do_ipsec_in_classify(&pkt, ctx, &skip);
 				ctx->state = (skip) ?
 					PKT_STATE_ROUTE_LOOKUP :
 					PKT_STATE_IPSEC_IN_FINISH;
@@ -1124,7 +1145,7 @@ void *pktio_thread(void *arg EXAMPLE_UNUSED)
 
 			case PKT_STATE_IPSEC_OUT_SEQ:
 
-				rc = do_ipsec_out_seq(pkt, ctx);
+				rc = do_ipsec_out_seq(&pkt, ctx);
 				ctx->state = PKT_STATE_IPSEC_OUT_FINISH;
 				break;
 
@@ -1150,7 +1171,6 @@ void *pktio_thread(void *arg EXAMPLE_UNUSED)
 		if ((PKT_DROP == rc) || (PKT_DONE == rc))
 			free_pkt_ctx(ctx);
 
-
 		/* Check for drop */
 		if (PKT_DROP == rc)
 			odph_packet_free(pkt);
diff --git a/platform/linux-generic/include/api/odp_crypto.h b/platform/linux-generic/include/api/odp_crypto.h
index 337e7cf..aac44d5 100644
--- a/platform/linux-generic/include/api/odp_crypto.h
+++ b/platform/linux-generic/include/api/odp_crypto.h
@@ -225,29 +225,25 @@ odp_crypto_session_create(odp_crypto_session_params_t *params,
 			  odp_crypto_session_t *session,
 			  enum odp_crypto_ses_create_err *status);
 
-
 /**
  * Crypto per packet operation
  *
  * Performs the cryptographic operations specified during session creation
- * on the packet.  If the operation is performed synchronously, "posted"
- * will return FALSE and the result of the operation is immediately available
- * in the completion event.  If "posted" returns TRUE the result will be
- * delivered via the completion queue specified when the session was created.
- *
- * @todo Resolve if completion_event is necessary, can/should the output
- *       packet buffer always be used instead.
+ * on the packet.  If the operation is performed synchronously, result will
+ * be returned in completion_event buffer immediately. In case of asynchronous
+ * operation completion event will return ODP_INVALID_BUFFER and the result
+ * will be delivered via the completion queue specified when the session was
+ * created.
  *
- * @param params            Operation parameters
- * @param posted            Pointer to return posted, TRUE for async operation
- * @param completion_event  Event by which the operation results are delivered.
+ * @param[in]  params            Operation parameters
+ * @param[out] completion_event  Pointer to completion event by which the
+ *                               synchronous operation results are delivered.
  *
  * @return 0 if successful else -1
  */
 int
 odp_crypto_operation(odp_crypto_op_params_t *params,
-		     bool *posted,
-		     odp_buffer_t completion_event);
+		     odp_buffer_t *completion_event);
 
 /**
  * Crypto per packet operation set user context in completion event
@@ -297,6 +293,18 @@ void *
 odp_crypto_get_operation_compl_ctx(odp_buffer_t completion_event);
 
 /**
+ * Free completion event
+ *
+ * Completion event is an opaque buffer that is managed by implementation.
+ * The event can't be freed directly, because it can point to an output packet.
+ * This function should be used instead.
+ *
+ * @param completion_event  Event to be freed
+ */
+void
+odp_crypto_operation_compl_free(odp_buffer_t completion_event);
+
+/**
  * Generate random byte string
  *
  * @param buf          Pointer to store result
diff --git a/platform/linux-generic/odp_crypto.c b/platform/linux-generic/odp_crypto.c
index d3cdec7..98cbdb7 100644
--- a/platform/linux-generic/odp_crypto.c
+++ b/platform/linux-generic/odp_crypto.c
@@ -337,15 +337,14 @@ odp_crypto_session_create(odp_crypto_session_params_t *params,
 
 int
 odp_crypto_operation(odp_crypto_op_params_t *params,
-		     bool *posted,
-		     odp_buffer_t completion_event)
+		     odp_buffer_t *completion_event)
 {
 	enum crypto_alg_err rc_cipher = ODP_CRYPTO_ALG_ERR_NONE;
 	enum crypto_alg_err rc_auth = ODP_CRYPTO_ALG_ERR_NONE;
 	odp_crypto_generic_session_t *session;
 	odp_crypto_generic_op_result_t *result;
+	odp_buffer_t completion;
 
-	*posted = 0;
 	session = (odp_crypto_generic_session_t *)(intptr_t)params->session;
 
 	/* Resolve output buffer */
@@ -357,13 +356,12 @@ odp_crypto_operation(odp_crypto_op_params_t *params,
 		if (odp_unlikely(ODP_PACKET_INVALID == params->out_pkt))
 			abort();
 		odp_packet_copy(params->out_pkt, params->pkt);
-		if (completion_event == odp_packet_to_buffer(params->pkt))
-			completion_event =
-				odp_packet_to_buffer(params->out_pkt);
 		odph_packet_free(params->pkt);
 		params->pkt = ODP_PACKET_INVALID;
 	}
 
+	completion = odp_packet_to_buffer(params->out_pkt);
+
 	/* Invoke the functions */
 	if (session->do_cipher_first) {
 		rc_cipher = session->cipher.func(params, session);
@@ -374,7 +372,7 @@ odp_crypto_operation(odp_crypto_op_params_t *params,
 	}
 
 	/* Build Result (no HW so no errors) */
-	result = get_op_result_from_buffer(completion_event);
+	result = get_op_result_from_buffer(completion);
 	result->magic = OP_RESULT_MAGIC;
 	result->cipher.alg_err = rc_cipher;
 	result->cipher.hw_err = ODP_CRYPTO_HW_ERR_NONE;
@@ -383,8 +381,10 @@ odp_crypto_operation(odp_crypto_op_params_t *params,
 
 	/* If specified during creation post event to completion queue */
 	if (ODP_QUEUE_INVALID != session->compl_queue) {
-		odp_queue_enq(session->compl_queue, completion_event);
-		*posted = 1;
+		odp_queue_enq(session->compl_queue, completion);
+		*completion_event = ODP_BUFFER_INVALID;
+	} else {
+		*completion_event = completion;
 	}
 	return 0;
 }
@@ -421,7 +421,6 @@ odp_hw_random_get(uint8_t *buf, size_t *len, bool use_entropy ODP_UNUSED)
 	rc = RAND_bytes(buf, *len);
 	return ((1 == rc) ? 0 : -1);
 }
-
 void
 odp_crypto_get_operation_compl_status(odp_buffer_t completion_event,
 				      odp_crypto_compl_status_t *auth,
@@ -454,8 +453,13 @@ void
 }
 
 odp_packet_t
-odp_crypto_get_operation_compl_packet(odp_buffer_t completion_event ODP_UNUSED)
+odp_crypto_get_operation_compl_packet(odp_buffer_t completion_event)
 {
-	ODP_UNIMPLEMENTED();
-	return ODP_PACKET_INVALID;
+	return odp_packet_from_buffer(completion_event);
+}
+
+void
+odp_crypto_operation_compl_free(odp_buffer_t completion_event ODP_UNUSED)
+{
+	/* Original packet is used as a completion, so nothing to free */
 }
